Подготовить repair-итерации из stage GUI handoff

This commit is contained in:
dctouch 2026-05-09 12:29:41 +03:00
parent 2244c62554
commit fdf70b3d4f
4 changed files with 291 additions and 0 deletions

View File

@ -27,6 +27,11 @@
- Use an integer `0%` to `100%` scale and keep the estimate architecture-aware, based on implemented runtime wiring, tests, replay coverage, and remaining integration risk. - Use an integer `0%` to `100%` scale and keep the estimate architecture-aware, based on implemented runtime wiring, tests, replay coverage, and remaining integration risk.
- Do not inflate progress because unit tests are green; semantic replay and real runtime wiring still count as unfinished work when they are pending. - Do not inflate progress because unit tests are green; semantic replay and real runtime wiring still count as unfinished work when they are pending.
## next_stage_plan_rule
- After applying fixes or completing a development stage, always provide `Дальше по плану:` in the close-out.
- The plan must list the next concrete module/stage action, the expected validation path, and whether the next step needs commit/push/manual replay.
- If the next plan is unclear or stale, the first next action must be to resync docs, graphify, and current stage artifacts before continuing implementation.
## graphify ## graphify
This project has a graphify knowledge graph at graphify-out/. This project has a graphify knowledge graph at graphify-out/.

View File

@ -93,6 +93,7 @@ Canonical commands:
python scripts/stage_agent_loop.py plan --manifest docs/orchestration/<stage_loop>.json python scripts/stage_agent_loop.py plan --manifest docs/orchestration/<stage_loop>.json
python scripts/stage_agent_loop.py run --manifest docs/orchestration/<stage_loop>.json python scripts/stage_agent_loop.py run --manifest docs/orchestration/<stage_loop>.json
python scripts/stage_agent_loop.py ingest-gui-run --manifest docs/orchestration/<stage_loop>.json --run-id assistant-stage1-<id> python scripts/stage_agent_loop.py ingest-gui-run --manifest docs/orchestration/<stage_loop>.json --run-id assistant-stage1-<id>
python scripts/stage_agent_loop.py prepare-repair --manifest docs/orchestration/<stage_loop>.json
python scripts/stage_agent_loop.py summarize --manifest docs/orchestration/<stage_loop>.json python scripts/stage_agent_loop.py summarize --manifest docs/orchestration/<stage_loop>.json
``` ```
@ -139,6 +140,14 @@ It stores the GUI review under `artifacts/domain_runs/stage_agent_loops/<stage_i
It also writes `stage_repair_handoff.md/json` next to the stage summary. That handoff is the preferred input for the next coder pass: it lists primary repair targets and sample user-facing failures without forcing the coder to reread the entire GUI conversation first. It also writes `stage_repair_handoff.md/json` next to the stage summary. That handoff is the preferred input for the next coder pass: it lists primary repair targets and sample user-facing failures without forcing the coder to reread the entire GUI conversation first.
To prepare the next repair iteration from that handoff, run:
```powershell
python scripts/stage_agent_loop.py prepare-repair --manifest docs/orchestration/<stage_loop>.json
```
This writes `repair_iterations/<iteration_id>/repair_iteration_plan.json`, `repair_prompt.md`, and `repair_checklist.md`. The plan enriches GUI repair targets with candidate runtime files and rerun instructions, so the next coder pass can start from a bounded business defect instead of a full transcript archaeology dig.
## Placeholder contract ## Placeholder contract
Scenario questions can reference earlier step outputs with placeholders such as: Scenario questions can reference earlier step outputs with placeholders such as:

View File

@ -10,6 +10,7 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import domain_case_loop as dcl
import review_assistant_stage1_run as gui_review import review_assistant_stage1_run as gui_review
@ -451,6 +452,142 @@ def save_stage_repair_handoff(stage_dir: Path, handoff: dict[str, Any]) -> None:
write_text(stage_dir / "stage_repair_handoff.md", build_stage_repair_handoff_markdown(handoff)) write_text(stage_dir / "stage_repair_handoff.md", build_stage_repair_handoff_markdown(handoff))
def enrich_repair_target_for_coder(target: dict[str, Any]) -> dict[str, Any]:
problem_layer = str(target.get("problem_layer") or "other").strip() or "other"
candidate_files = list(dcl.REPAIR_TARGET_FILE_HINTS.get(problem_layer) or dcl.REPAIR_TARGET_FILE_HINTS["other"])
return {
**target,
"candidate_files": candidate_files,
}
def build_stage_repair_iteration_plan(
*,
stage_manifest: dict[str, Any],
stage_dir: Path,
handoff: dict[str, Any],
iteration_id: str,
) -> dict[str, Any]:
primary_targets = handoff.get("primary_repair_targets") if isinstance(handoff.get("primary_repair_targets"), list) else []
enriched_targets = [
enrich_repair_target_for_coder(target)
for target in primary_targets
if isinstance(target, dict)
]
sample_findings = handoff.get("sample_findings") if isinstance(handoff.get("sample_findings"), list) else []
candidate_files = list(
dict.fromkeys(
path
for target in enriched_targets
for path in target.get("candidate_files", [])
if isinstance(path, str) and path.strip()
)
)
return {
"schema_version": "stage_gui_repair_iteration_plan_v1",
"stage_id": stage_manifest["stage_id"],
"module_name": stage_manifest.get("module_name"),
"title": stage_manifest.get("title"),
"iteration_id": iteration_id,
"created_at": now_iso(),
"stage_dir": repo_relative(stage_dir),
"source_handoff": repo_relative(stage_dir / "stage_repair_handoff.json"),
"source_review_markdown": handoff.get("review_markdown"),
"source_repair_targets_json": handoff.get("repair_targets_json"),
"run_id": handoff.get("run_id"),
"next_action": handoff.get("next_action"),
"overall_business_status": handoff.get("overall_business_status"),
"p0_findings": handoff.get("p0_findings"),
"p1_findings": handoff.get("p1_findings"),
"question_quality_score": handoff.get("question_quality_score"),
"primary_repair_targets": enriched_targets,
"candidate_files": candidate_files,
"sample_findings": [finding for finding in sample_findings if isinstance(finding, dict)][:8],
"acceptance_rerun": {
"after_patch": [
"run focused unit tests for touched backend/runtime code",
"run graphify rebuild after code changes",
"rerun the same GUI/user session or equivalent stage pack",
"ingest the new assistant-stage1 run id with stage_agent_loop.py ingest-gui-run",
"accept only when P0 is zero and P1 noise is either cleared or explicitly bounded",
],
"ingest_command_template": (
"python scripts/stage_agent_loop.py ingest-gui-run "
"--manifest <stage_manifest.json> --run-id assistant-stage1-<new_id>"
),
},
}
def build_stage_repair_prompt(plan: dict[str, Any]) -> str:
return (
"You are repairing the NDC_1C assistant from a stage-level GUI semantic review.\n\n"
"Use the repo rules from `AGENTS.md` and `.codex/skills/domain-case-loop/SKILL.md`.\n\n"
"Goal:\n"
"- Make the smallest safe runtime/domain patch that clears the primary repair targets.\n"
"- Preserve successful flows and avoid architecture drift.\n"
"- User-facing business meaning is the acceptance surface; route ids and tests are supporting evidence only.\n\n"
"Hard rules:\n"
"- Do not fabricate 1C data.\n"
"- Do not hide unsupported facts behind confident prose.\n"
"- Keep direct answers first for direct business questions.\n"
"- Keep methodology/evidence after the answer, not above it.\n"
"- Do not touch unrelated files.\n"
"- After code changes, run relevant tests and graphify rebuild.\n\n"
"Repair iteration plan JSON:\n"
"```json\n"
f"{json.dumps(plan, ensure_ascii=False, indent=2)}\n"
"```\n\n"
"Required outputs for the coding pass:\n"
"- A minimal patch in the working tree.\n"
"- A short patch summary.\n"
"- Verification commands and results.\n"
"- The next GUI/stage rerun command to validate the same semantic defect class.\n"
)
def build_stage_repair_checklist(plan: dict[str, Any]) -> str:
lines = [
"# Stage Repair Checklist",
"",
f"- stage_id: `{plan.get('stage_id')}`",
f"- iteration_id: `{plan.get('iteration_id')}`",
f"- source_run_id: `{plan.get('run_id')}`",
f"- next_action: `{plan.get('next_action')}`",
"",
"## Candidate Files",
]
files = plan.get("candidate_files") if isinstance(plan.get("candidate_files"), list) else []
lines.extend([f"- `{path}`" for path in files] if files else ["- none"])
lines.extend(["", "## Primary Targets"])
targets = plan.get("primary_repair_targets") if isinstance(plan.get("primary_repair_targets"), list) else []
if not targets:
lines.append("- no repair targets")
else:
for target in targets:
if not isinstance(target, dict):
continue
lines.append(
f"- `{target.get('severity')}` `{target.get('problem_layer')}` / `{target.get('issue_code')}`: "
f"{target.get('occurrences')} occurrence(s)"
)
lines.extend(["", "## Acceptance"])
acceptance = plan.get("acceptance_rerun") if isinstance(plan.get("acceptance_rerun"), dict) else {}
for item in acceptance.get("after_patch", []) if isinstance(acceptance.get("after_patch"), list) else []:
lines.append(f"- {item}")
lines.append(f"- ingest template: `{acceptance.get('ingest_command_template') or 'n/a'}`")
return "\n".join(lines).strip() + "\n"
def save_stage_repair_iteration(stage_dir: Path, plan: dict[str, Any]) -> Path:
iteration_id = str(plan.get("iteration_id") or "repair_iteration").strip()
iteration_dir = stage_dir / "repair_iterations" / slugify(iteration_id)
write_json(iteration_dir / "repair_iteration_plan.json", plan)
write_text(iteration_dir / "repair_prompt.md", build_stage_repair_prompt(plan))
write_text(iteration_dir / "repair_checklist.md", build_stage_repair_checklist(plan))
return iteration_dir
def handle_ingest_gui_run(args: argparse.Namespace) -> int: def handle_ingest_gui_run(args: argparse.Namespace) -> int:
stage_manifest_path = repo_path(args.manifest) stage_manifest_path = repo_path(args.manifest)
stage_manifest = load_stage_manifest(stage_manifest_path) stage_manifest = load_stage_manifest(stage_manifest_path)
@ -485,6 +622,34 @@ def handle_ingest_gui_run(args: argparse.Namespace) -> int:
return 0 return 0
def handle_prepare_repair(args: argparse.Namespace) -> int:
stage_manifest_path = repo_path(args.manifest)
stage_manifest = load_stage_manifest(stage_manifest_path)
stage_dir = stage_dir_for(repo_path(args.output_root), stage_manifest["stage_id"])
handoff_path = repo_path(args.handoff) if args.handoff else stage_dir / "stage_repair_handoff.json"
handoff = load_json_object(handoff_path, "Stage repair handoff")
iteration_id = str(args.iteration_id or f"repair_{slugify(str(handoff.get('run_id') or 'gui_run'))}").strip()
plan = build_stage_repair_iteration_plan(
stage_manifest=stage_manifest,
stage_dir=stage_dir,
handoff=handoff,
iteration_id=iteration_id,
)
iteration_dir = save_stage_repair_iteration(stage_dir, plan)
payload = {
"schema_version": "stage_gui_repair_prepare_result_v1",
"stage_id": stage_manifest["stage_id"],
"iteration_id": iteration_id,
"iteration_dir": repo_relative(iteration_dir),
"repair_plan": repo_relative(iteration_dir / "repair_iteration_plan.json"),
"repair_prompt": repo_relative(iteration_dir / "repair_prompt.md"),
"repair_checklist": repo_relative(iteration_dir / "repair_checklist.md"),
"candidate_files": plan.get("candidate_files") or [],
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0
def handle_plan(args: argparse.Namespace) -> int: def handle_plan(args: argparse.Namespace) -> int:
stage_manifest_path = repo_path(args.manifest) stage_manifest_path = repo_path(args.manifest)
stage_manifest = load_stage_manifest(stage_manifest_path) stage_manifest = load_stage_manifest(stage_manifest_path)
@ -611,6 +776,15 @@ def build_parser() -> argparse.ArgumentParser:
ingest_parser.add_argument("--reports-dir", default=str(gui_review.DEFAULT_REPORTS_DIR)) ingest_parser.add_argument("--reports-dir", default=str(gui_review.DEFAULT_REPORTS_DIR))
ingest_parser.add_argument("--review-output-dir") ingest_parser.add_argument("--review-output-dir")
ingest_parser.set_defaults(func=handle_ingest_gui_run) ingest_parser.set_defaults(func=handle_ingest_gui_run)
repair_parser = subparsers.add_parser(
"prepare-repair",
help="Build coder-ready repair iteration artifacts from stage_repair_handoff.json.",
)
add_common_args(repair_parser)
repair_parser.add_argument("--handoff")
repair_parser.add_argument("--iteration-id")
repair_parser.set_defaults(func=handle_prepare_repair)
return parser return parser

View File

@ -244,6 +244,109 @@ class StageAgentLoopTests(unittest.TestCase):
self.assertEqual(handoff["primary_repair_targets"][0]["issue_code"], "business_direct_answer_missing") self.assertEqual(handoff["primary_repair_targets"][0]["issue_code"], "business_direct_answer_missing")
self.assertEqual(handoff["sample_findings"][0]["turn_index"], 19) self.assertEqual(handoff["sample_findings"][0]["turn_index"], 19)
def test_repair_iteration_plan_adds_candidate_files(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
stage_dir = Path(tmp) / "stage"
handoff = {
"run_id": "assistant-stage1-test",
"next_action": "continue_repair_from_gui_review_p0",
"overall_business_status": "fail",
"p0_findings": 1,
"p1_findings": 0,
"question_quality_score": 100,
"review_markdown": "review.md",
"repair_targets_json": "repair_targets.json",
"primary_repair_targets": [
{
"problem_layer": "answer_shape_mismatch",
"issue_code": "business_direct_answer_missing",
"severity": "P0",
"occurrences": 2,
}
],
"sample_findings": [
{
"turn_index": 19,
"severity": "P0",
"issue_codes": ["business_direct_answer_missing"],
"question": "какой у нас самый доходный год",
"assistant_first_line": "Коротко: Ограниченный бизнес-обзор...",
}
],
}
plan = stage_loop.build_stage_repair_iteration_plan(
stage_manifest={
"stage_id": "agent_loop",
"module_name": "Agent Loop",
"title": "Agent Loop",
},
stage_dir=stage_dir,
handoff=handoff,
iteration_id="repair_001",
)
self.assertEqual(plan["iteration_id"], "repair_001")
self.assertIn("llm_normalizer/backend/src/services/address_runtime/composeStage.ts", plan["candidate_files"])
self.assertEqual(plan["primary_repair_targets"][0]["candidate_files"][0], "llm_normalizer/backend/src/services/address_runtime/composeStage.ts")
self.assertIn("ingest-gui-run", plan["acceptance_rerun"]["ingest_command_template"])
def test_handle_prepare_repair_materializes_prompt_and_checklist(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
manifest_path = root / "stage.json"
output_root = root / "stage_runs"
stage_dir = output_root / "agent_loop"
write_json(
manifest_path,
{
"stage_id": "agent_loop",
"module_name": "Agent Loop",
"title": "Agent Loop",
"pack_manifest": "docs/orchestration/demo_pack.json",
},
)
write_json(
stage_dir / "stage_repair_handoff.json",
{
"run_id": "assistant-stage1-test",
"next_action": "continue_repair_from_gui_review_p0",
"overall_business_status": "fail",
"p0_findings": 1,
"p1_findings": 0,
"question_quality_score": 100,
"review_markdown": "review.md",
"repair_targets_json": "repair_targets.json",
"primary_repair_targets": [
{
"problem_layer": "business_utility_gap",
"issue_code": "business_answer_too_verbose",
"severity": "P1",
"occurrences": 4,
}
],
"sample_findings": [],
},
)
exit_code = stage_loop.handle_prepare_repair(
stage_args(
manifest=str(manifest_path),
output_root=str(output_root),
handoff=None,
iteration_id="repair_001",
)
)
iteration_dir = stage_dir / "repair_iterations" / "repair_001"
plan_exists = (iteration_dir / "repair_iteration_plan.json").exists()
prompt = (iteration_dir / "repair_prompt.md").read_text(encoding="utf-8")
checklist_exists = (iteration_dir / "repair_checklist.md").exists()
self.assertEqual(exit_code, 0)
self.assertTrue(plan_exists)
self.assertTrue(checklist_exists)
self.assertIn("business_answer_too_verbose", prompt)
def test_handle_ingest_gui_run_materializes_stage_review(self) -> None: def test_handle_ingest_gui_run_materializes_stage_review(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)