ОРРКЕСТРАЦИЯ - Оркестрация домена: ужесточить автофикс loop и назначать primary repair focus
This commit is contained in:
@@ -7,7 +7,10 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from scripts.domain_case_loop import (
|
||||
build_coder_loop_prompt,
|
||||
build_coder_snapshot_paths,
|
||||
build_deterministic_repair_targets,
|
||||
build_scenario_step_state,
|
||||
build_scenario_acceptance_matrix,
|
||||
carry_forward_analysis_context,
|
||||
derive_pack_final_status,
|
||||
@@ -15,6 +18,9 @@ from scripts.domain_case_loop import (
|
||||
evaluate_deterministic_loop_gate,
|
||||
load_scenario_pack,
|
||||
merge_scenario_date_scope,
|
||||
select_primary_repair_focus,
|
||||
restore_line_collapsed_files_from_snapshot,
|
||||
snapshot_coder_candidate_files,
|
||||
validate_step_contract,
|
||||
)
|
||||
|
||||
@@ -582,6 +588,191 @@ def test_build_deterministic_repair_targets_marks_anchor_gap_as_p1() -> None:
|
||||
assert "addressQueryService.ts" in " ".join(target["candidate_files"])
|
||||
|
||||
|
||||
def test_build_deterministic_repair_targets_prioritizes_high_leverage_focus() -> None:
|
||||
repair_targets = build_deterministic_repair_targets(
|
||||
{"pack_id": "demo_pack", "domain": "inventory_stock", "final_status": "partial"},
|
||||
[
|
||||
{
|
||||
"scenario_id": "inventory_aging_and_unresolved",
|
||||
"title": "Aging and unresolved",
|
||||
"artifact_dir": "artifacts/domain_runs/demo/scenarios/inventory_aging_and_unresolved",
|
||||
"scenario_state": {
|
||||
"step_outputs": {
|
||||
"step_05_unresolved_supplier_link": {
|
||||
"step_id": "step_05_unresolved_supplier_link",
|
||||
"question_resolved": "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
|
||||
"execution_status": "exact",
|
||||
"acceptance_status": "rejected",
|
||||
"reply_type": "factual",
|
||||
"selected_recipe": "address_inventory_supplier_stock_overlap_as_of_date_v1",
|
||||
"capability_id": "inventory_inventory_supplier_stock_overlap_as_of_date",
|
||||
"violated_invariants": [
|
||||
"wrong_as_of_date",
|
||||
"missing_required_filter",
|
||||
"wrong_date_scope_state",
|
||||
],
|
||||
"warnings": [],
|
||||
"hard_fail": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"scenario_id": "inventory_snapshot_roots",
|
||||
"title": "Root stock snapshots",
|
||||
"artifact_dir": "artifacts/domain_runs/demo/scenarios/inventory_snapshot_roots",
|
||||
"scenario_state": {
|
||||
"step_outputs": {
|
||||
"step_01_stock_now": {
|
||||
"step_id": "step_01_stock_now",
|
||||
"question_resolved": "Какие товары сейчас лежат на складе",
|
||||
"execution_status": "exact",
|
||||
"acceptance_status": "rejected",
|
||||
"reply_type": "factual",
|
||||
"selected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||
"capability_id": "confirmed_inventory_on_hand_as_of_date",
|
||||
"violated_invariants": [
|
||||
"wrong_as_of_date",
|
||||
"missing_required_filter",
|
||||
],
|
||||
"warnings": [],
|
||||
"hard_fail": True,
|
||||
},
|
||||
"step_02_stock_on_historical_date": {
|
||||
"step_id": "step_02_stock_on_historical_date",
|
||||
"question_resolved": "Покажи остатки на складе на март 2019",
|
||||
"execution_status": "exact",
|
||||
"acceptance_status": "rejected",
|
||||
"reply_type": "factual",
|
||||
"selected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||
"capability_id": "confirmed_inventory_on_hand_as_of_date",
|
||||
"violated_invariants": [
|
||||
"wrong_as_of_date",
|
||||
"wrong_period_from",
|
||||
"wrong_period_to",
|
||||
],
|
||||
"warnings": [],
|
||||
"hard_fail": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert repair_targets["target_count"] == 3
|
||||
assert repair_targets["priority_foci"][0]["problem_type"] == "temporal_honesty_gap"
|
||||
assert repair_targets["priority_foci"][0]["target_count"] == 2
|
||||
assert repair_targets["targets"][0]["problem_type"] == "temporal_honesty_gap"
|
||||
assert repair_targets["targets"][0]["repair_focus_rank"] == 1
|
||||
|
||||
|
||||
def test_build_coder_loop_prompt_demands_high_leverage_focus_first(tmp_path) -> None:
|
||||
prompt = build_coder_loop_prompt(
|
||||
loop_dir=tmp_path / "loop",
|
||||
iteration_dir=tmp_path / "loop" / "iterations" / "iteration_00",
|
||||
pack_dir=tmp_path / "loop" / "iterations" / "iteration_00" / "pack_output" / "pack_run",
|
||||
repair_targets_path=tmp_path / "loop" / "iterations" / "iteration_00" / "pack_output" / "pack_run" / "repair_targets.json",
|
||||
repair_targets_json='{"priority_foci":[{"focus_rank":1,"problem_type":"temporal_honesty_gap","target_count":4}]}',
|
||||
assigned_focus={"focus_id": "temporal_honesty_gap|addressFilterExtractor.ts", "problem_type": "temporal_honesty_gap"},
|
||||
analyst_verdict_path=tmp_path / "loop" / "iterations" / "iteration_00" / "analyst_verdict.json",
|
||||
analyst_verdict_json='{"quality_score":56}',
|
||||
)
|
||||
|
||||
assert "highest-leverage repair focus first" in prompt
|
||||
assert "patch the narrowest shared layer" in prompt
|
||||
assert "single-line collapses" in prompt
|
||||
assert "mandatory for this iteration" in prompt
|
||||
assert "temporal_honesty_gap|addressFilterExtractor.ts" in prompt
|
||||
|
||||
|
||||
def test_select_primary_repair_focus_returns_top_priority_focus() -> None:
|
||||
focus = select_primary_repair_focus(
|
||||
{
|
||||
"priority_foci": [
|
||||
{"focus_id": "focus-1", "focus_rank": 1},
|
||||
{"focus_id": "focus-2", "focus_rank": 2},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert focus == {"focus_id": "focus-1", "focus_rank": 1}
|
||||
|
||||
|
||||
def test_build_coder_snapshot_paths_collects_candidate_files_once(tmp_path) -> None:
|
||||
repo_root = tmp_path
|
||||
file_a = repo_root / "llm_normalizer/backend/src/services/addressFilterExtractor.ts"
|
||||
file_b = repo_root / "llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts"
|
||||
file_a.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_b.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_a.write_text("line1\nline2\n", encoding="utf-8")
|
||||
file_b.write_text("line1\nline2\n", encoding="utf-8")
|
||||
|
||||
original_repo_root = sys.modules["scripts.domain_case_loop"].REPO_ROOT
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = repo_root
|
||||
try:
|
||||
paths = build_coder_snapshot_paths(
|
||||
{
|
||||
"priority_foci": [
|
||||
{
|
||||
"candidate_files": [
|
||||
"llm_normalizer/backend/src/services/addressFilterExtractor.ts",
|
||||
"llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts",
|
||||
]
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"candidate_files": [
|
||||
"llm_normalizer/backend/src/services/addressFilterExtractor.ts",
|
||||
"../outside.ts",
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
finally:
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = original_repo_root
|
||||
|
||||
assert paths == [file_a, file_b]
|
||||
|
||||
|
||||
def test_restore_line_collapsed_files_from_snapshot_recovers_original_text(tmp_path) -> None:
|
||||
sample = tmp_path / "sample.ts"
|
||||
original = "const a = 1;\nconst b = 2;\n"
|
||||
sample.write_text(original, encoding="utf-8")
|
||||
snapshots = snapshot_coder_candidate_files([sample])
|
||||
sample.write_text("const a = 1;const b = 2;", encoding="utf-8")
|
||||
|
||||
original_repo_root = sys.modules["scripts.domain_case_loop"].REPO_ROOT
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = tmp_path
|
||||
try:
|
||||
restored = restore_line_collapsed_files_from_snapshot(snapshots)
|
||||
finally:
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = original_repo_root
|
||||
|
||||
assert restored == ["sample.ts"]
|
||||
assert sample.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
def test_restore_line_collapsed_files_from_snapshot_keeps_semantic_changes(tmp_path) -> None:
|
||||
sample = tmp_path / "sample.ts"
|
||||
original = "const a = 1;\nconst b = 2;\n"
|
||||
sample.write_text(original, encoding="utf-8")
|
||||
snapshots = snapshot_coder_candidate_files([sample])
|
||||
sample.write_text("const a = 1;const b = 3;", encoding="utf-8")
|
||||
|
||||
original_repo_root = sys.modules["scripts.domain_case_loop"].REPO_ROOT
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = tmp_path
|
||||
try:
|
||||
restored = restore_line_collapsed_files_from_snapshot(snapshots)
|
||||
finally:
|
||||
sys.modules["scripts.domain_case_loop"].REPO_ROOT = original_repo_root
|
||||
|
||||
assert restored == []
|
||||
assert sample.read_text(encoding="utf-8") == "const a = 1;const b = 3;"
|
||||
|
||||
|
||||
def test_evaluate_deterministic_loop_gate_rejects_partial_pack_even_without_targets() -> None:
|
||||
gate_ok, reason = evaluate_deterministic_loop_gate(
|
||||
{"final_status": "partial"},
|
||||
@@ -610,3 +801,83 @@ def test_evaluate_deterministic_loop_gate_accepts_clean_pack_without_remaining_p
|
||||
|
||||
assert gate_ok is True
|
||||
assert reason == "deterministic_gate_passed"
|
||||
|
||||
|
||||
def test_build_scenario_step_state_uses_effective_analysis_context_from_turn_artifact() -> None:
|
||||
step_state = build_scenario_step_state(
|
||||
scenario_id="inventory_snapshot_roots",
|
||||
domain="inventory_stock",
|
||||
step={
|
||||
"step_id": "step_03_account_41_now",
|
||||
"title": "Account 41 current composition",
|
||||
"depends_on": [],
|
||||
"question_template": "Из каких товаров состоит остаток по 41 счету",
|
||||
"analysis_context": {},
|
||||
"expected_intents": ["inventory_on_hand_as_of_date"],
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_recipe": None,
|
||||
"expected_result_mode": "confirmed_balance",
|
||||
"required_filters": {
|
||||
"period_from": "2021-09-01",
|
||||
"period_to": "2021-09-30",
|
||||
},
|
||||
"forbidden_capabilities": [],
|
||||
"forbidden_recipes": [],
|
||||
"required_state_objects": [],
|
||||
"required_answer_shape": "item_list_with_account_41_scope",
|
||||
"forbidden_answer_patterns": [],
|
||||
"required_carryover_invariants": [],
|
||||
"invariant_severity": {},
|
||||
},
|
||||
step_index=3,
|
||||
question_resolved="Из каких товаров состоит остаток по 41 счету",
|
||||
analysis_context={"as_of_date": "2021-09-30", "source": "scenario_manifest"},
|
||||
turn_artifact={
|
||||
"scenario": {
|
||||
"analysis_context": {
|
||||
"as_of_date": "2021-09-30",
|
||||
"source": "scenario_manifest",
|
||||
}
|
||||
},
|
||||
"assistant_message": {
|
||||
"reply_type": "factual",
|
||||
"text": "На 31.03.2019 на складе подтверждено 16 позиций.",
|
||||
},
|
||||
"technical_debug_payload": {
|
||||
"detected_mode": "address_query",
|
||||
"detected_intent": "inventory_on_hand_as_of_date",
|
||||
"selected_recipe": "address_inventory_on_hand_as_of_date_v1",
|
||||
"capability_id": "confirmed_inventory_on_hand_as_of_date",
|
||||
"capability_route_mode": "exact",
|
||||
"route_expectation_status": "matched",
|
||||
"result_mode": "confirmed_balance",
|
||||
"response_type": "FACTUAL_LIST",
|
||||
"extracted_filters": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"period_from": "2019-03-01",
|
||||
"period_to": "2019-03-31",
|
||||
},
|
||||
"fallback_type": "none",
|
||||
"mcp_call_status": "matched_non_empty",
|
||||
"balance_confirmed": True,
|
||||
},
|
||||
"session_summary": {
|
||||
"address_navigation_state": {
|
||||
"session_context": {
|
||||
"date_scope": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"period_from": "2019-03-01",
|
||||
"period_to": "2019-03-31",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
entries=[],
|
||||
)
|
||||
|
||||
assert step_state["analysis_context"]["as_of_date"] == "2021-09-30"
|
||||
assert "wrong_as_of_date" in step_state["violated_invariants"]
|
||||
assert "wrong_period_from" in step_state["violated_invariants"]
|
||||
assert "wrong_period_to" in step_state["violated_invariants"]
|
||||
assert step_state["acceptance_status"] == "rejected"
|
||||
|
||||
Reference in New Issue
Block a user