Подготовить repair-итерации из stage GUI handoff
This commit is contained in:
parent
2244c62554
commit
fdf70b3d4f
|
|
@ -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/.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue