Разделить проверенные route candidates в domain-loop

This commit is contained in:
dctouch 2026-06-03 13:35:19 +03:00
parent ed44c6428a
commit 584edb7e5b
2 changed files with 128 additions and 1 deletions

View File

@ -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: def build_route_candidate_focus_signature(candidate: dict[str, Any]) -> str:
status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown" 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" 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]: ) -> dict[str, Any]:
targets: list[dict[str, Any]] = [] targets: list[dict[str, Any]] = []
route_candidates: 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]] = {} step_validation_index: dict[str, dict[str, Any]] = {}
for scenario_artifact in scenario_artifacts: for scenario_artifact in scenario_artifacts:
scenario_id = str(scenario_artifact.get("scenario_id") or "").strip() scenario_id = str(scenario_artifact.get("scenario_id") or "").strip()
@ -4832,7 +4849,10 @@ def build_deterministic_repair_targets(
step_output=raw_step_output, step_output=raw_step_output,
) )
if route_candidate: 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( target = build_step_repair_target(
scenario_id=scenario_id, scenario_id=scenario_id,
scenario_title=scenario_title, scenario_title=scenario_title,
@ -4887,6 +4907,11 @@ def build_deterministic_repair_targets(
for candidate in route_candidates: for candidate in route_candidates:
status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown" status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown"
route_candidate_status_counts[status] = route_candidate_status_counts.get(status, 0) + 1 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 { return {
"schema_version": "domain_pack_repair_targets_v1", "schema_version": "domain_pack_repair_targets_v1",
"pack_id": pack_state.get("pack_id"), "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_handoff_count": len(route_candidates),
"route_candidate_status_counts": route_candidate_status_counts, "route_candidate_status_counts": route_candidate_status_counts,
"route_candidate_groups": route_candidate_groups, "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"- 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_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"- 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 [] route_candidate_groups = repair_targets.get("route_candidate_groups") or []
if isinstance(route_candidate_groups, list) and route_candidate_groups: 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'}`", 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 [] priority_foci = repair_targets.get("priority_foci") or []
if isinstance(priority_foci, list) and priority_foci: if isinstance(priority_foci, list) and priority_foci:
lines.extend( lines.extend(

View File

@ -286,6 +286,86 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertIn("## Route Candidate Handoff Groups", markdown) self.assertIn("## Route Candidate Handoff Groups", markdown)
self.assertIn("route_candidate_demo:step_01", 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: def test_analysis_context_date_is_not_implicit_business_filter(self) -> None:
step_state = dcl.build_scenario_step_state( step_state = dcl.build_scenario_step_state(
scenario_id="stage_pack_demo", scenario_id="stage_pack_demo",