Показать причины блокировки auto-coder в handoff

This commit is contained in:
dctouch 2026-05-24 12:53:48 +03:00
parent 81acca3332
commit 06e035eadf
4 changed files with 91 additions and 1 deletions

View File

@ -5508,6 +5508,7 @@ def build_lead_coder_handoff(
def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str: def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str:
artifact_refs = handoff.get("artifact_refs") if isinstance(handoff.get("artifact_refs"), dict) else {} artifact_refs = handoff.get("artifact_refs") if isinstance(handoff.get("artifact_refs"), dict) else {}
human_meaning = handoff.get("human_meaning") if isinstance(handoff.get("human_meaning"), dict) else {} human_meaning = handoff.get("human_meaning") if isinstance(handoff.get("human_meaning"), dict) else {}
auto_coder_gate = handoff.get("auto_coder_gate") if isinstance(handoff.get("auto_coder_gate"), dict) else {}
lines = [ lines = [
"# Lead Codex repair handoff", "# Lead Codex repair handoff",
"", "",
@ -5534,13 +5535,51 @@ def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str:
f"- issue_codes: `{', '.join(normalize_string_list(handoff.get('issue_codes'))) or 'n/a'}`", f"- issue_codes: `{', '.join(normalize_string_list(handoff.get('issue_codes'))) or 'n/a'}`",
f"- rerun_matrix: `{', '.join(normalize_string_list(handoff.get('rerun_matrix'))) or 'n/a'}`", f"- rerun_matrix: `{', '.join(normalize_string_list(handoff.get('rerun_matrix'))) or 'n/a'}`",
"", "",
]
if auto_coder_gate:
lines.extend(
[
"## Auto-Coder Gate",
f"- allowed: `{bool(auto_coder_gate.get('allowed'))}`",
f"- reason: `{auto_coder_gate.get('reason') or 'n/a'}`",
f"- focus_id: `{auto_coder_gate.get('focus_id') or 'n/a'}`",
]
)
blocking_reasons = normalize_string_list(auto_coder_gate.get("blocking_reasons"))
lines.extend([f"- blocking_reason: `{item}`" for item in blocking_reasons[:12]] or ["- blocking_reason: `none`"])
if len(blocking_reasons) > 12:
lines.append(f"- blocking_reason_extra_count: `{len(blocking_reasons) - 12}`")
issue_contracts = (
auto_coder_gate.get("issue_catalog_contracts")
if isinstance(auto_coder_gate.get("issue_catalog_contracts"), dict)
else {}
)
if issue_contracts:
lines.extend(["", "## Auto-Coder Catalog Contracts"])
for issue_code, contract in sorted(issue_contracts.items()):
if not isinstance(contract, dict):
continue
lines.extend(
[
f"- issue_code: `{issue_code}`",
f" expected_contract: `{contract.get('expected_answer_contract') or 'n/a'}`",
f" root_layers: `{', '.join(normalize_string_list(contract.get('root_layers'))) or 'n/a'}`",
f" allowed_patch_targets: `{', '.join(normalize_string_list(contract.get('allowed_patch_targets'))) or 'n/a'}`",
f" forbidden_patch_targets: `{', '.join(normalize_string_list(contract.get('forbidden_patch_targets'))) or 'n/a'}`",
f" rerun_matrix: `{', '.join(normalize_string_list(contract.get('rerun_matrix'))) or 'n/a'}`",
]
)
lines.append("")
lines.extend(
[
"## Human Meaning", "## Human Meaning",
f"- user_intent_summary: {human_meaning.get('user_intent_summary') or 'n/a'}", f"- user_intent_summary: {human_meaning.get('user_intent_summary') or 'n/a'}",
f"- expected_direct_answer: {human_meaning.get('expected_direct_answer') or 'n/a'}", f"- expected_direct_answer: {human_meaning.get('expected_direct_answer') or 'n/a'}",
f"- actual_direct_answer: {human_meaning.get('actual_direct_answer') or 'n/a'}", f"- actual_direct_answer: {human_meaning.get('actual_direct_answer') or 'n/a'}",
"", "",
"## Primary Focus", "## Primary Focus",
] ]
)
assigned_focus = handoff.get("assigned_primary_focus") if isinstance(handoff.get("assigned_primary_focus"), dict) else {} assigned_focus = handoff.get("assigned_primary_focus") if isinstance(handoff.get("assigned_primary_focus"), dict) else {}
if assigned_focus: if assigned_focus:
candidate_files = normalize_string_list(assigned_focus.get("candidate_files")) candidate_files = normalize_string_list(assigned_focus.get("candidate_files"))

View File

@ -860,6 +860,7 @@ def build_stage_summary(
"last_deterministic_gate_ok": last_iteration.get("deterministic_gate_ok"), "last_deterministic_gate_ok": last_iteration.get("deterministic_gate_ok"),
"last_deterministic_gate_reason": last_iteration.get("deterministic_gate_reason"), "last_deterministic_gate_reason": last_iteration.get("deterministic_gate_reason"),
"latest_business_audit": repo_relative(Path(str(last_iteration.get("business_audit_path")))) if last_iteration.get("business_audit_path") else None, "latest_business_audit": repo_relative(Path(str(last_iteration.get("business_audit_path")))) if last_iteration.get("business_audit_path") else None,
"latest_auto_coder_gate": repo_relative(Path(str(last_iteration.get("auto_coder_gate_path")))) if last_iteration.get("auto_coder_gate_path") else None,
"latest_lead_coder_handoff": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")))) if (last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")) else None, "latest_lead_coder_handoff": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")))) if (last_iteration.get("lead_coder_handoff_markdown_path") or loop_state.get("latest_lead_coder_handoff_markdown_path")) else None,
"latest_lead_coder_handoff_json": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")))) if (last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")) else None, "latest_lead_coder_handoff_json": repo_relative(Path(str(last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")))) if (last_iteration.get("lead_coder_handoff_path") or loop_state.get("latest_lead_coder_handoff_path")) else None,
"loop_accepted_gate": bool(last_iteration.get("accepted_gate")), "loop_accepted_gate": bool(last_iteration.get("accepted_gate")),
@ -970,6 +971,7 @@ def build_stage_handoff_markdown(summary: dict[str, Any]) -> str:
f"- next_action: `{summary.get('next_action')}`", f"- next_action: `{summary.get('next_action')}`",
f"- loop_dir: `{summary.get('loop_dir')}`", f"- loop_dir: `{summary.get('loop_dir')}`",
f"- latest_business_audit: `{summary.get('latest_business_audit') or 'n/a'}`", f"- latest_business_audit: `{summary.get('latest_business_audit') or 'n/a'}`",
f"- latest_auto_coder_gate: `{summary.get('latest_auto_coder_gate') or 'n/a'}`",
f"- latest_lead_coder_handoff: `{summary.get('latest_lead_coder_handoff') or 'n/a'}`", f"- latest_lead_coder_handoff: `{summary.get('latest_lead_coder_handoff') or 'n/a'}`",
f"- stop_reason: {summary.get('stop_reason') or 'n/a'}", f"- stop_reason: {summary.get('stop_reason') or 'n/a'}",
"", "",

View File

@ -288,6 +288,53 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
gate["blocking_reasons"], gate["blocking_reasons"],
) )
def test_lead_handoff_markdown_surfaces_auto_coder_gate_blockers(self) -> None:
handoff = {
"repair_mode": "lead-handoff",
"loop_id": "demo_loop",
"iteration_id": "iteration_00",
"quality_score": 42,
"target_score": 88,
"loop_decision": "partial",
"deterministic_gate_ok": False,
"deterministic_gate_reason": "repair_targets_remaining=P0:1",
"artifact_refs": {
"business_audit": "artifacts/domain_runs/demo/business_audit.md",
"analyst_verdict": "artifacts/domain_runs/demo/analyst_verdict.json",
"repair_targets": "artifacts/domain_runs/demo/repair_targets.json",
"auto_coder_gate": "artifacts/domain_runs/demo/auto_coder_gate.json",
"pack_dir": "artifacts/domain_runs/demo/pack",
},
"issue_codes": ["business_direct_answer_missing"],
"rerun_matrix": ["failed_scenario", "accepted_smoke_pack"],
"human_meaning": {"user_intent_summary": "User needs a direct answer."},
"top_repair_targets": [],
"candidate_files": [],
"lead_instructions": [],
"auto_coder_gate": {
"allowed": False,
"reason": "target_missing_evidence_paths:pack:s01",
"focus_id": "answer_shape|composeStage",
"blocking_reasons": ["target_missing_evidence_paths:pack:s01"],
"issue_catalog_contracts": {
"business_direct_answer_missing": {
"expected_answer_contract": "direct_answer_surface_v1",
"root_layers": ["answer_surface"],
"allowed_patch_targets": ["llm_normalizer/backend/src/services/address_runtime/composeStage.ts"],
"forbidden_patch_targets": ["routing rewrites"],
"rerun_matrix": ["failed_scenario", "accepted_smoke_pack"],
}
},
},
}
markdown = dcl.build_lead_coder_handoff_markdown(handoff)
self.assertIn("## Auto-Coder Gate", markdown)
self.assertIn("target_missing_evidence_paths:pack:s01", markdown)
self.assertIn("## Auto-Coder Catalog Contracts", markdown)
self.assertIn("direct_answer_surface_v1", markdown)
def test_analyst_priority_targets_become_lead_repair_targets(self) -> None: def test_analyst_priority_targets_become_lead_repair_targets(self) -> None:
repair_targets = { repair_targets = {
"pack_id": "demo_pack", "pack_id": "demo_pack",

View File

@ -227,6 +227,7 @@ class StageAgentLoopTests(unittest.TestCase):
"accepted_gate": False, "accepted_gate": False,
"deterministic_gate_ok": False, "deterministic_gate_ok": False,
"business_audit_path": str(iteration_dir / "business_audit.md"), "business_audit_path": str(iteration_dir / "business_audit.md"),
"auto_coder_gate_path": str(iteration_dir / "auto_coder_gate.json"),
"lead_coder_handoff_markdown_path": str(iteration_dir / "lead_coder_handoff.md"), "lead_coder_handoff_markdown_path": str(iteration_dir / "lead_coder_handoff.md"),
"coder_status": "lead_handoff_required", "coder_status": "lead_handoff_required",
} }
@ -249,6 +250,7 @@ class StageAgentLoopTests(unittest.TestCase):
self.assertEqual(summary["next_action"], "lead_coder_repair_required") self.assertEqual(summary["next_action"], "lead_coder_repair_required")
self.assertIn("lead_coder_handoff", summary["latest_lead_coder_handoff"]) self.assertIn("lead_coder_handoff", summary["latest_lead_coder_handoff"])
self.assertIn("business_audit", summary["latest_business_audit"]) self.assertIn("business_audit", summary["latest_business_audit"])
self.assertIn("auto_coder_gate", summary["latest_auto_coder_gate"])
def test_build_stage_summary_blocks_close_when_repair_lacks_validation(self) -> None: def test_build_stage_summary_blocks_close_when_repair_lacks_validation(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp: