diff --git a/scripts/domain_case_loop.py b/scripts/domain_case_loop.py index 1fcb874..660f4be 100644 --- a/scripts/domain_case_loop.py +++ b/scripts/domain_case_loop.py @@ -4592,6 +4592,22 @@ def compact_route_candidate_handoff( } +def route_candidate_requires_handoff(step_output: dict[str, Any]) -> bool: + status = str(step_output.get("mcp_discovery_route_candidate_status") or "").strip() + if status in {"needs_route_enablement", "needs_user_scope"}: + return True + if status != "ready_for_reviewed_execution": + return False + acceptance_status = str(step_output.get("acceptance_status") or step_output.get("status") or "").strip() + if acceptance_status not in {"validated", "accepted"}: + return True + if normalize_string_list(step_output.get("violated_invariants")): + return True + if normalize_string_list(step_output.get("warnings")): + return True + return False + + def build_route_candidate_focus_signature(candidate: dict[str, Any]) -> str: status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown" selected_chain_id = str(candidate.get("selected_chain_id") or "no_chain").strip() or "no_chain" @@ -4810,6 +4826,7 @@ def build_deterministic_repair_targets( ) -> dict[str, Any]: targets: list[dict[str, Any]] = [] route_candidates: list[dict[str, Any]] = [] + reviewed_route_candidates: list[dict[str, Any]] = [] step_validation_index: dict[str, dict[str, Any]] = {} for scenario_artifact in scenario_artifacts: scenario_id = str(scenario_artifact.get("scenario_id") or "").strip() @@ -4832,7 +4849,10 @@ def build_deterministic_repair_targets( step_output=raw_step_output, ) if route_candidate: - route_candidates.append(route_candidate) + if route_candidate_requires_handoff(raw_step_output): + route_candidates.append(route_candidate) + else: + reviewed_route_candidates.append(route_candidate) target = build_step_repair_target( scenario_id=scenario_id, scenario_title=scenario_title, @@ -4887,6 +4907,11 @@ def build_deterministic_repair_targets( for candidate in route_candidates: status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown" route_candidate_status_counts[status] = route_candidate_status_counts.get(status, 0) + 1 + reviewed_route_candidate_groups = build_route_candidate_handoff_groups(reviewed_route_candidates) + reviewed_route_candidate_status_counts: dict[str, int] = {} + for candidate in reviewed_route_candidates: + status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown" + reviewed_route_candidate_status_counts[status] = reviewed_route_candidate_status_counts.get(status, 0) + 1 return { "schema_version": "domain_pack_repair_targets_v1", "pack_id": pack_state.get("pack_id"), @@ -4908,6 +4933,9 @@ def build_deterministic_repair_targets( "route_candidate_handoff_count": len(route_candidates), "route_candidate_status_counts": route_candidate_status_counts, "route_candidate_groups": route_candidate_groups, + "reviewed_route_candidate_count": len(reviewed_route_candidates), + "reviewed_route_candidate_status_counts": reviewed_route_candidate_status_counts, + "reviewed_route_candidate_groups": reviewed_route_candidate_groups, } @@ -5555,6 +5583,8 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str: f"- severity_counts: `{dump_json(repair_targets.get('severity_counts') or {})}`", f"- route_candidate_handoff_count: `{repair_targets.get('route_candidate_handoff_count') if 'route_candidate_handoff_count' in repair_targets else 0}`", f"- route_candidate_status_counts: `{dump_json(repair_targets.get('route_candidate_status_counts') or {})}`", + f"- reviewed_route_candidate_count: `{repair_targets.get('reviewed_route_candidate_count') if 'reviewed_route_candidate_count' in repair_targets else 0}`", + f"- reviewed_route_candidate_status_counts: `{dump_json(repair_targets.get('reviewed_route_candidate_status_counts') or {})}`", ] route_candidate_groups = repair_targets.get("route_candidate_groups") or [] if isinstance(route_candidate_groups, list) and route_candidate_groups: @@ -5573,6 +5603,23 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str: f" target_ids: `{', '.join(normalize_string_list(group.get('target_ids'))) or 'n/a'}`", ] ) + reviewed_route_candidate_groups = repair_targets.get("reviewed_route_candidate_groups") or [] + if isinstance(reviewed_route_candidate_groups, list) and reviewed_route_candidate_groups: + lines.extend(["", "## Reviewed Route Candidates (No Handoff)"]) + for group in reviewed_route_candidate_groups: + if not isinstance(group, dict): + continue + lines.extend( + [ + f"- group_id: `{group.get('group_id') or 'n/a'}`", + f" candidate_count: `{group.get('candidate_count') or 0}`", + f" status: `{group.get('candidate_status') or 'n/a'}`", + f" selected_chain_id: `{group.get('selected_chain_id') or 'n/a'}`", + f" missing_axes: `{', '.join(normalize_string_list(group.get('missing_axes'))) or 'n/a'}`", + f" executable_now: `{group.get('executable_now')}`", + f" target_ids: `{', '.join(normalize_string_list(group.get('target_ids'))) or 'n/a'}`", + ] + ) priority_foci = repair_targets.get("priority_foci") or [] if isinstance(priority_foci, list) and priority_foci: lines.extend( diff --git a/scripts/test_domain_case_loop_step_state.py b/scripts/test_domain_case_loop_step_state.py index 77926f6..9b4cac2 100644 --- a/scripts/test_domain_case_loop_step_state.py +++ b/scripts/test_domain_case_loop_step_state.py @@ -286,6 +286,86 @@ class DomainCaseLoopStepStateTests(unittest.TestCase): self.assertIn("## Route Candidate Handoff Groups", markdown) self.assertIn("route_candidate_demo:step_01", markdown) + def test_validated_ready_route_candidates_are_reviewed_without_repair_handoff(self) -> None: + repair_targets = dcl.build_deterministic_repair_targets( + {"pack_id": "route_candidate_pack", "domain": "inventory_stock", "final_status": "accepted"}, + [ + { + "scenario_id": "inventory_sale_trace", + "title": "Sale trace", + "artifact_dir": "artifacts/domain_runs/inventory_sale_trace", + "scenario_state": { + "step_outputs": { + "step_04": { + "status": "validated", + "acceptance_status": "validated", + "execution_status": "exact", + "question_resolved": "trace purchase to sale", + "reply_type": "factual", + "mcp_discovery_selected_chain_id": "metadata_inspection", + "mcp_discovery_route_candidate_status": "ready_for_reviewed_execution", + "mcp_discovery_route_candidate_fact_family": "schema_surface", + "mcp_discovery_route_candidate_action_family": "inspect_surface", + "mcp_discovery_route_candidate_missing_axes": ["metadata_scope"], + "mcp_discovery_route_candidate_executable_now": True, + "violated_invariants": [], + "warnings": [], + } + } + }, + } + ], + ) + + self.assertEqual(repair_targets["target_count"], 0) + self.assertEqual(repair_targets["route_candidate_handoff_count"], 0) + self.assertEqual(repair_targets["route_candidate_status_counts"], {}) + self.assertEqual(repair_targets["route_candidate_groups"], []) + self.assertEqual(repair_targets["reviewed_route_candidate_count"], 1) + self.assertEqual(repair_targets["reviewed_route_candidate_status_counts"], {"ready_for_reviewed_execution": 1}) + self.assertEqual( + repair_targets["reviewed_route_candidate_groups"][0]["group_id"], + "ready_for_reviewed_execution|metadata_inspection|metadata_scope", + ) + + def test_nonvalidated_ready_route_candidates_stay_in_repair_handoff(self) -> None: + repair_targets = dcl.build_deterministic_repair_targets( + {"pack_id": "route_candidate_pack", "domain": "inventory_stock", "final_status": "partial"}, + [ + { + "scenario_id": "inventory_sale_trace", + "title": "Sale trace", + "artifact_dir": "artifacts/domain_runs/inventory_sale_trace", + "scenario_state": { + "step_outputs": { + "step_04": { + "status": "rejected", + "acceptance_status": "rejected", + "execution_status": "partial", + "question_resolved": "trace purchase to sale", + "reply_type": "partial_coverage", + "mcp_discovery_selected_chain_id": "metadata_inspection", + "mcp_discovery_route_candidate_status": "ready_for_reviewed_execution", + "mcp_discovery_route_candidate_fact_family": "schema_surface", + "mcp_discovery_route_candidate_action_family": "inspect_surface", + "mcp_discovery_route_candidate_missing_axes": ["metadata_scope"], + "mcp_discovery_route_candidate_executable_now": True, + "violated_invariants": ["answer_shape_mismatch"], + "warnings": [], + } + } + }, + } + ], + ) + + self.assertEqual(repair_targets["route_candidate_handoff_count"], 1) + self.assertEqual(repair_targets["reviewed_route_candidate_count"], 0) + self.assertEqual( + repair_targets["route_candidate_groups"][0]["group_id"], + "ready_for_reviewed_execution|metadata_inspection|metadata_scope", + ) + def test_analysis_context_date_is_not_implicit_business_filter(self) -> None: step_state = dcl.build_scenario_step_state( scenario_id="stage_pack_demo",