ОРРКЕСТРАЦИЯ - Оркестрация домена: ужесточить автофикс loop и назначать primary repair focus
This commit is contained in:
+238
-5
@@ -61,6 +61,19 @@ DEFAULT_INVARIANT_SEVERITY: dict[str, str] = {
|
||||
}
|
||||
|
||||
REPAIR_TARGET_SEVERITY_ORDER = {"P0": 0, "P1": 1, "P2": 2}
|
||||
REPAIR_TARGET_PROBLEM_ORDER = {
|
||||
"temporal_honesty_gap": 0,
|
||||
"edge_carryover_gap": 1,
|
||||
"followup_action_resolution_gap": 2,
|
||||
"object_memory_gap": 3,
|
||||
"route_gap": 4,
|
||||
"answer_shape_mismatch": 5,
|
||||
"presentation_gap": 6,
|
||||
"domain_anchor_gap": 7,
|
||||
"capability_gap": 8,
|
||||
"evidence_gap": 9,
|
||||
"other": 10,
|
||||
}
|
||||
|
||||
REPAIR_TARGET_FILE_HINTS: dict[str, list[str]] = {
|
||||
"followup_action_resolution_gap": [
|
||||
@@ -132,6 +145,65 @@ def read_text_file(file_path: Path) -> str:
|
||||
return file_path.read_text(encoding="utf-8-sig")
|
||||
|
||||
|
||||
def resolve_repo_relative_path(raw_path: str | None) -> Path | None:
|
||||
candidate = str(raw_path or "").strip().replace("\\", "/")
|
||||
if not candidate:
|
||||
return None
|
||||
path = Path(candidate)
|
||||
if path.is_absolute() or any(part == ".." for part in path.parts):
|
||||
return None
|
||||
return REPO_ROOT / path
|
||||
|
||||
|
||||
def build_coder_snapshot_paths(repair_targets: dict[str, Any]) -> list[Path]:
|
||||
collected: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
groups = []
|
||||
if isinstance(repair_targets, dict):
|
||||
groups.extend(repair_targets.get("priority_foci") or [])
|
||||
groups.extend(repair_targets.get("targets") or [])
|
||||
for item in groups:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for raw_path in normalize_string_list(item.get("candidate_files")):
|
||||
resolved = resolve_repo_relative_path(raw_path)
|
||||
if resolved is None or not resolved.exists() or resolved in seen:
|
||||
continue
|
||||
seen.add(resolved)
|
||||
collected.append(resolved)
|
||||
return collected
|
||||
|
||||
|
||||
def snapshot_coder_candidate_files(paths: list[Path]) -> dict[str, bytes]:
|
||||
snapshots: dict[str, bytes] = {}
|
||||
for path in paths:
|
||||
if not path.exists() or not path.is_file():
|
||||
continue
|
||||
snapshots[str(path)] = path.read_bytes()
|
||||
return snapshots
|
||||
|
||||
|
||||
def restore_line_collapsed_files_from_snapshot(snapshots: dict[str, bytes]) -> list[str]:
|
||||
restored: list[str] = []
|
||||
for raw_path, before_bytes in snapshots.items():
|
||||
path = Path(raw_path)
|
||||
if not path.exists() or not path.is_file():
|
||||
continue
|
||||
after_bytes = path.read_bytes()
|
||||
before_line_count = before_bytes.count(b"\n")
|
||||
after_line_count = after_bytes.count(b"\n")
|
||||
if before_line_count < 1 or after_line_count != 0:
|
||||
continue
|
||||
if before_bytes.replace(b"\r", b"").replace(b"\n", b"") != after_bytes.replace(b"\r", b"").replace(b"\n", b""):
|
||||
continue
|
||||
path.write_bytes(before_bytes)
|
||||
try:
|
||||
restored.append(str(path.relative_to(REPO_ROOT)).replace("\\", "/"))
|
||||
except ValueError:
|
||||
restored.append(str(path))
|
||||
return restored
|
||||
|
||||
|
||||
def sanitize_export_text(value: str) -> str:
|
||||
raw = str(value or "")
|
||||
debug_heading = re.search(
|
||||
@@ -1551,6 +1623,7 @@ def build_scenario_step_state(
|
||||
step: dict[str, Any],
|
||||
step_index: int,
|
||||
question_resolved: str,
|
||||
analysis_context: dict[str, Any],
|
||||
turn_artifact: dict[str, Any],
|
||||
entries: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
@@ -1570,7 +1643,12 @@ def build_scenario_step_state(
|
||||
reply_type = assistant_item.get("reply_type")
|
||||
assistant_text = str(assistant_item.get("text") or "")
|
||||
top_non_empty = first_non_empty_lines(assistant_text, limit=3)
|
||||
analysis_context = step.get("analysis_context") if isinstance(step.get("analysis_context"), dict) else {}
|
||||
turn_scenario = turn_artifact.get("scenario") if isinstance(turn_artifact.get("scenario"), dict) else {}
|
||||
effective_analysis_context = normalize_analysis_context(turn_scenario.get("analysis_context"))
|
||||
if not effective_analysis_context:
|
||||
effective_analysis_context = normalize_analysis_context(analysis_context)
|
||||
if not effective_analysis_context:
|
||||
effective_analysis_context = step.get("analysis_context") if isinstance(step.get("analysis_context"), dict) else {}
|
||||
|
||||
step_state = {
|
||||
"schema_version": SCENARIO_STEP_STATE_SCHEMA_VERSION,
|
||||
@@ -1582,7 +1660,7 @@ def build_scenario_step_state(
|
||||
"depends_on": step["depends_on"],
|
||||
"question_template": step["question_template"],
|
||||
"question_resolved": question_resolved,
|
||||
"analysis_context": analysis_context,
|
||||
"analysis_context": effective_analysis_context,
|
||||
"expected_intents": step.get("expected_intents") or [],
|
||||
"expected_capability": step.get("expected_capability"),
|
||||
"expected_recipe": step.get("expected_recipe"),
|
||||
@@ -1789,6 +1867,7 @@ def run_assistant_step(
|
||||
step=step,
|
||||
step_index=step_index,
|
||||
question_resolved=question_resolved,
|
||||
analysis_context=analysis_context,
|
||||
turn_artifact=turn_artifact,
|
||||
entries=entries,
|
||||
)
|
||||
@@ -2764,6 +2843,63 @@ def build_step_repair_target(
|
||||
}
|
||||
|
||||
|
||||
def build_repair_focus_signature(target: dict[str, Any]) -> str:
|
||||
problem_type = str(target.get("problem_type") or "other").strip() or "other"
|
||||
candidate_files = normalize_string_list(target.get("candidate_files"))
|
||||
primary_file = candidate_files[0] if candidate_files else "no_file_hint"
|
||||
return f"{problem_type}|{primary_file}"
|
||||
|
||||
|
||||
def build_priority_repair_foci(targets: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, dict[str, Any]] = {}
|
||||
for target in targets:
|
||||
focus_id = build_repair_focus_signature(target)
|
||||
focus = grouped.setdefault(
|
||||
focus_id,
|
||||
{
|
||||
"focus_id": focus_id,
|
||||
"severity": str(target.get("severity") or "P2"),
|
||||
"problem_type": str(target.get("problem_type") or "other"),
|
||||
"root_cause_layers": normalize_string_list(target.get("root_cause_layers")),
|
||||
"candidate_files": normalize_string_list(target.get("candidate_files")),
|
||||
"target_ids": [],
|
||||
"scenario_ids": set(),
|
||||
},
|
||||
)
|
||||
focus["target_ids"].append(str(target.get("target_id") or ""))
|
||||
scenario_id = str(target.get("scenario_id") or "").strip()
|
||||
if scenario_id:
|
||||
focus["scenario_ids"].add(scenario_id)
|
||||
|
||||
priority_foci: list[dict[str, Any]] = []
|
||||
for focus in grouped.values():
|
||||
scenario_ids = sorted(focus.pop("scenario_ids"))
|
||||
target_ids = [target_id for target_id in focus.get("target_ids", []) if target_id]
|
||||
focus["target_count"] = len(target_ids)
|
||||
focus["scenario_count"] = len(scenario_ids)
|
||||
focus["target_ids"] = target_ids
|
||||
focus["scenario_ids"] = scenario_ids
|
||||
priority_foci.append(focus)
|
||||
|
||||
priority_foci.sort(
|
||||
key=lambda item: (
|
||||
REPAIR_TARGET_SEVERITY_ORDER.get(str(item.get("severity") or "P2"), 99),
|
||||
-int(item.get("target_count") or 0),
|
||||
-int(item.get("scenario_count") or 0),
|
||||
REPAIR_TARGET_PROBLEM_ORDER.get(str(item.get("problem_type") or "other"), 99),
|
||||
str(item.get("focus_id") or ""),
|
||||
)
|
||||
)
|
||||
for index, focus in enumerate(priority_foci, start=1):
|
||||
primary_file = normalize_string_list(focus.get("candidate_files"))[:1]
|
||||
focus["focus_rank"] = index
|
||||
focus["rank_reason"] = (
|
||||
f"severity={focus.get('severity')} targets={focus.get('target_count')} "
|
||||
f"scenarios={focus.get('scenario_count')} primary_file={primary_file[0] if primary_file else 'n/a'}"
|
||||
)
|
||||
return priority_foci
|
||||
|
||||
|
||||
def build_deterministic_repair_targets(
|
||||
pack_state: dict[str, Any],
|
||||
scenario_artifacts: list[dict[str, Any]],
|
||||
@@ -2792,9 +2928,36 @@ def build_deterministic_repair_targets(
|
||||
if target:
|
||||
targets.append(target)
|
||||
|
||||
priority_foci = build_priority_repair_foci(targets)
|
||||
focus_rank_by_id = {
|
||||
str(focus.get("focus_id") or ""): int(focus.get("focus_rank") or 999)
|
||||
for focus in priority_foci
|
||||
if isinstance(focus, dict)
|
||||
}
|
||||
focus_target_count_by_id = {
|
||||
str(focus.get("focus_id") or ""): int(focus.get("target_count") or 0)
|
||||
for focus in priority_foci
|
||||
if isinstance(focus, dict)
|
||||
}
|
||||
focus_scenario_count_by_id = {
|
||||
str(focus.get("focus_id") or ""): int(focus.get("scenario_count") or 0)
|
||||
for focus in priority_foci
|
||||
if isinstance(focus, dict)
|
||||
}
|
||||
for target in targets:
|
||||
focus_id = build_repair_focus_signature(target)
|
||||
target["repair_focus_id"] = focus_id
|
||||
target["repair_focus_rank"] = focus_rank_by_id.get(focus_id, 999)
|
||||
target["repair_focus_target_count"] = focus_target_count_by_id.get(focus_id, 0)
|
||||
target["repair_focus_scenario_count"] = focus_scenario_count_by_id.get(focus_id, 0)
|
||||
|
||||
targets.sort(
|
||||
key=lambda item: (
|
||||
REPAIR_TARGET_SEVERITY_ORDER.get(str(item.get("severity") or "P2"), 99),
|
||||
int(item.get("repair_focus_rank") or 999),
|
||||
-int(item.get("repair_focus_target_count") or 0),
|
||||
-int(item.get("repair_focus_scenario_count") or 0),
|
||||
REPAIR_TARGET_PROBLEM_ORDER.get(str(item.get("problem_type") or "other"), 99),
|
||||
str(item.get("scenario_id") or ""),
|
||||
str(item.get("step_id") or ""),
|
||||
)
|
||||
@@ -2811,10 +2974,21 @@ def build_deterministic_repair_targets(
|
||||
"final_status": pack_state.get("final_status"),
|
||||
"target_count": len(targets),
|
||||
"severity_counts": severity_counts,
|
||||
"priority_foci": priority_foci,
|
||||
"targets": targets,
|
||||
}
|
||||
|
||||
|
||||
def select_primary_repair_focus(repair_targets: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if not isinstance(repair_targets, dict):
|
||||
return None
|
||||
priority_foci = repair_targets.get("priority_foci")
|
||||
if not isinstance(priority_foci, list) or not priority_foci:
|
||||
return None
|
||||
primary_focus = priority_foci[0]
|
||||
return primary_focus if isinstance(primary_focus, dict) else None
|
||||
|
||||
|
||||
def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# Repair targets",
|
||||
@@ -2823,9 +2997,36 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str:
|
||||
f"- domain: `{repair_targets.get('domain') or 'n/a'}`",
|
||||
f"- target_count: `{repair_targets.get('target_count') or 0}`",
|
||||
f"- severity_counts: `{dump_json(repair_targets.get('severity_counts') or {})}`",
|
||||
"",
|
||||
"## Targets",
|
||||
]
|
||||
priority_foci = repair_targets.get("priority_foci") or []
|
||||
if isinstance(priority_foci, list) and priority_foci:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Priority foci",
|
||||
]
|
||||
)
|
||||
for focus in priority_foci:
|
||||
if not isinstance(focus, dict):
|
||||
continue
|
||||
lines.extend(
|
||||
[
|
||||
f"- `{focus.get('focus_id')}`",
|
||||
f" focus_rank: `{focus.get('focus_rank')}`",
|
||||
f" severity: `{focus.get('severity')}`",
|
||||
f" problem_type: `{focus.get('problem_type')}`",
|
||||
f" target_count: `{focus.get('target_count')}`",
|
||||
f" scenario_count: `{focus.get('scenario_count')}`",
|
||||
f" candidate_files: {', '.join(focus.get('candidate_files') or []) or 'none'}",
|
||||
f" rank_reason: {focus.get('rank_reason') or 'n/a'}",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Targets",
|
||||
]
|
||||
)
|
||||
for target in repair_targets.get("targets") or []:
|
||||
if not isinstance(target, dict):
|
||||
continue
|
||||
@@ -2834,6 +3035,8 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str:
|
||||
f"- `{target.get('target_id')}`",
|
||||
f" severity: `{target.get('severity')}`",
|
||||
f" problem_type: `{target.get('problem_type')}`",
|
||||
f" repair_focus_rank: `{target.get('repair_focus_rank')}`",
|
||||
f" repair_focus_target_count: `{target.get('repair_focus_target_count')}`",
|
||||
f" root_cause_layers: {', '.join(target.get('root_cause_layers') or []) or 'none'}",
|
||||
f" fix_goal: {target.get('fix_goal') or 'n/a'}",
|
||||
f" candidate_files: {', '.join(target.get('candidate_files') or []) or 'none'}",
|
||||
@@ -2989,6 +3192,7 @@ def build_analyst_loop_prompt(
|
||||
- `accepted` is allowed only if quality_score >= {target_score}, unresolved_p0_count = 0, and regression_detected = false;
|
||||
- `accepted` is forbidden if the evidence bundle shows `pack_state.final_status != accepted` or the deterministic repair targets still contain any `P0` or `P1` items;
|
||||
- `accepted` also requires `direct_answer_ok = true`, `business_usefulness_ok = true`, `temporal_honesty_ok = true`, and `field_truth_ok = true`;
|
||||
- when several failing steps share one deterministic repair focus, call out the highest-leverage shared focus first instead of only the lexicographically first failing step;
|
||||
- `partial` means the pack is usable but exactness, routing, or coverage is still insufficient;
|
||||
- `needs_exact_capability` means the primary blocker is a missing exact route or capability, but the loop should still continue autonomously unless a user decision is required;
|
||||
- `continue` means there is a clear next patch cycle;
|
||||
@@ -3035,9 +3239,22 @@ def build_coder_loop_prompt(
|
||||
pack_dir: Path,
|
||||
repair_targets_path: Path,
|
||||
repair_targets_json: str,
|
||||
assigned_focus: dict[str, Any] | None,
|
||||
analyst_verdict_path: Path,
|
||||
analyst_verdict_json: str,
|
||||
) -> str:
|
||||
assigned_focus_block = (
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
Assigned deterministic repair focus for this iteration:
|
||||
```json
|
||||
{dump_json(assigned_focus)}
|
||||
```
|
||||
"""
|
||||
).strip()
|
||||
if assigned_focus
|
||||
else "Assigned deterministic repair focus for this iteration: none"
|
||||
)
|
||||
return textwrap.dedent(
|
||||
f"""\
|
||||
You are the `domain_coder` for NDC_1C.
|
||||
@@ -3061,8 +3278,11 @@ def build_coder_loop_prompt(
|
||||
- do not present heuristic answers as confirmed;
|
||||
- do not touch unrelated files;
|
||||
- preserve already successful baseline flows.
|
||||
- preserve UTF-8 without BOM and the existing line structure of edited files; do not leave whole-file normalization-only rewrites or single-line collapses in the worktree;
|
||||
- use minimal local edits; if a tool rewrites a file into normalization noise, restore the original file first and then apply only the intended semantic patch;
|
||||
- use `root_cause_layers`, `broken_edge_ids`, `violated_invariants`, and business-utility scores from the analyst verdict to choose the smallest fix;
|
||||
- use the deterministic repair targets to choose the narrowest failing edge before touching broader scenarios;
|
||||
- use the deterministic repair targets to choose the highest-leverage repair focus first; within that focus, patch the narrowest shared layer that can clear the most `P0`/`P1` targets without architecture drift;
|
||||
- the assigned deterministic repair focus below is mandatory for this iteration; do not switch to a lower-priority focus unless you are blocked from making a safe patch for the assigned focus;
|
||||
- if the analyst verdict is optimistic but deterministic repair targets still contain `P0` or `P1`, trust the deterministic repair targets and keep fixing the pack;
|
||||
- prioritize state continuity, selected-object persistence, stable `focus_object`, stable `answer_object`, reusable `provenance_bundle` / `sale_trace_bundle`, action-first answer behavior, compact micro-action answers, answer layering, temporal honesty, and field-truth mapping when those are the blocking layers;
|
||||
- do not broaden scope when the analyst says the defect is mainly `object_memory_gap`, `followup_action_resolution_gap`, `bundle_reuse_gap`, `field_mapping_gap`, `temporal_honesty_gap`, `answer_shape_mismatch`, or `business_utility_gap`;
|
||||
@@ -3082,6 +3302,8 @@ def build_coder_loop_prompt(
|
||||
{repair_targets_json}
|
||||
```
|
||||
|
||||
{assigned_focus_block}
|
||||
|
||||
- then return JSON only and follow the schema exactly.
|
||||
"""
|
||||
).strip()
|
||||
@@ -3218,6 +3440,8 @@ def build_loop_summary(loop_state: dict[str, Any]) -> str:
|
||||
f" requires_user_decision: `{item.get('requires_user_decision')}`",
|
||||
f" user_decision_type: `{item.get('user_decision_type') or 'none'}`",
|
||||
f" coder_status: `{item.get('coder_status') or 'n/a'}`",
|
||||
f" assigned_repair_focus_id: `{item.get('assigned_repair_focus_id') or 'none'}`",
|
||||
f" coder_workspace_hygiene_restored_files: `{', '.join(item.get('coder_workspace_hygiene_restored_files') or []) or 'none'}`",
|
||||
f" analyst_verdict: `{item.get('analyst_verdict_path') or 'n/a'}`",
|
||||
f" repair_targets: `{item.get('repair_targets_path') or 'n/a'}`",
|
||||
f" repair_target_count: `{item.get('repair_target_count')}`",
|
||||
@@ -3396,16 +3620,20 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
|
||||
break
|
||||
|
||||
coder_result_path = iteration_dir / "coder_result.json"
|
||||
assigned_focus = select_primary_repair_focus(repair_targets)
|
||||
coder_prompt = build_coder_loop_prompt(
|
||||
loop_dir=loop_dir,
|
||||
iteration_dir=iteration_dir,
|
||||
pack_dir=pack_dir,
|
||||
repair_targets_path=repair_targets_path,
|
||||
repair_targets_json=repair_targets_json,
|
||||
assigned_focus=assigned_focus,
|
||||
analyst_verdict_path=analyst_verdict_path,
|
||||
analyst_verdict_json=dump_json(analyst_verdict),
|
||||
)
|
||||
write_text(iteration_dir / "coder_prompt.md", coder_prompt + "\n")
|
||||
coder_snapshot_paths = build_coder_snapshot_paths(repair_targets)
|
||||
coder_snapshots = snapshot_coder_candidate_files(coder_snapshot_paths)
|
||||
coder_command = build_codex_exec_command(
|
||||
args,
|
||||
output_file=coder_result_path,
|
||||
@@ -3422,10 +3650,15 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
|
||||
stdout_path=iteration_dir / "coder_exec.stdout.log",
|
||||
stderr_path=iteration_dir / "coder_exec.stderr.log",
|
||||
)
|
||||
restored_files = restore_line_collapsed_files_from_snapshot(coder_snapshots)
|
||||
coder_result = read_json_output(coder_result_path)
|
||||
coder_status = str(coder_result.get("status") or "").strip() or "unknown"
|
||||
iteration_record["coder_status"] = coder_status
|
||||
iteration_record["coder_result_path"] = str(coder_result_path)
|
||||
if assigned_focus:
|
||||
iteration_record["assigned_repair_focus_id"] = str(assigned_focus.get("focus_id") or "")
|
||||
if restored_files:
|
||||
iteration_record["coder_workspace_hygiene_restored_files"] = restored_files
|
||||
loop_state["iterations"].append(iteration_record)
|
||||
loop_state["updated_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
write_json(loop_dir / "loop_state.json", loop_state)
|
||||
|
||||
Reference in New Issue
Block a user