Усилить агентный detector gate для declared detectors
This commit is contained in:
parent
bebea0361b
commit
9e2bc8cb6c
|
|
@ -65,6 +65,12 @@
|
||||||
"check": {
|
"check": {
|
||||||
"required_patterns_any": [
|
"required_patterns_any": [
|
||||||
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
|
"(?i)(выруч|себестоим|валов|марж|не хватает|не могу подтвердить|не подтвержден|unknown)"
|
||||||
|
],
|
||||||
|
"artifact_path_exclude_patterns": [
|
||||||
|
"(?i)selected_item",
|
||||||
|
"(?i)supplier",
|
||||||
|
"(?i)purchase_date",
|
||||||
|
"(?i)purchase_document"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,22 @@ def filter_turns(turns: list[dict[str, Any]], artifact_dir: Path, evidence_paths
|
||||||
return [item for item in turns if path_matches_evidence(item["path"], artifact_dir, evidence_paths)]
|
return [item for item in turns if path_matches_evidence(item["path"], artifact_dir, evidence_paths)]
|
||||||
|
|
||||||
|
|
||||||
|
def filter_artifacts_by_detector_scope(items: list[dict[str, Any]], check: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
include_patterns = compile_patterns(normalize_string_list(check.get("artifact_path_include_patterns")))
|
||||||
|
exclude_patterns = compile_patterns(normalize_string_list(check.get("artifact_path_exclude_patterns")))
|
||||||
|
if not include_patterns and not exclude_patterns:
|
||||||
|
return items
|
||||||
|
filtered: list[dict[str, Any]] = []
|
||||||
|
for item in items:
|
||||||
|
artifact_path = str(item.get("artifact_path") or item.get("repo_path") or "")
|
||||||
|
if include_patterns and not any(pattern.search(artifact_path) for pattern in include_patterns):
|
||||||
|
continue
|
||||||
|
if exclude_patterns and any(pattern.search(artifact_path) for pattern in exclude_patterns):
|
||||||
|
continue
|
||||||
|
filtered.append(item)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def compile_patterns(patterns: list[str]) -> list[re.Pattern[str]]:
|
def compile_patterns(patterns: list[str]) -> list[re.Pattern[str]]:
|
||||||
compiled: list[re.Pattern[str]] = []
|
compiled: list[re.Pattern[str]] = []
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
|
|
@ -591,8 +607,8 @@ def evaluate_detector(
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
kind = str(detector.get("kind") or "").strip()
|
kind = str(detector.get("kind") or "").strip()
|
||||||
check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
|
check = detector.get("check") if isinstance(detector.get("check"), dict) else {}
|
||||||
scoped_outputs = filter_outputs(outputs, artifact_dir, evidence_paths)
|
scoped_outputs = filter_artifacts_by_detector_scope(filter_outputs(outputs, artifact_dir, evidence_paths), check)
|
||||||
scoped_turns = filter_turns(turns, artifact_dir, evidence_paths)
|
scoped_turns = filter_artifacts_by_detector_scope(filter_turns(turns, artifact_dir, evidence_paths), check)
|
||||||
if kind == "artifact_presence":
|
if kind == "artifact_presence":
|
||||||
return evaluate_artifact_presence(detector_name, detector, artifact_dir)
|
return evaluate_artifact_presence(detector_name, detector, artifact_dir)
|
||||||
if kind == "answer_text_regex_forbidden":
|
if kind == "answer_text_regex_forbidden":
|
||||||
|
|
|
||||||
|
|
@ -6107,11 +6107,45 @@ def build_accepted_pack_smoke_detector_candidates() -> list[dict[str, Any]]:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str, Any] | None = None) -> dict[str, Any]:
|
def build_detector_candidates(
|
||||||
|
repair_targets: dict[str, Any],
|
||||||
|
catalog: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
declared_detectors: list[str] | None = None,
|
||||||
|
declared_issue_codes: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
source = catalog if isinstance(catalog, dict) else load_issue_catalog()
|
source = catalog if isinstance(catalog, dict) else load_issue_catalog()
|
||||||
issues = source.get("issues") if isinstance(source.get("issues"), dict) else {}
|
issues = source.get("issues") if isinstance(source.get("issues"), dict) else {}
|
||||||
candidates: list[dict[str, Any]] = []
|
candidates: list[dict[str, Any]] = []
|
||||||
seen: set[tuple[str, str]] = set()
|
seen: set[tuple[str, str]] = set()
|
||||||
|
|
||||||
|
def add_candidate(
|
||||||
|
*,
|
||||||
|
issue_code: str,
|
||||||
|
detector: str,
|
||||||
|
severity: Any = None,
|
||||||
|
sample_target_id: str | None = None,
|
||||||
|
evidence_paths: list[str] | None = None,
|
||||||
|
source_label: str,
|
||||||
|
) -> None:
|
||||||
|
detector_name = str(detector or "").strip()
|
||||||
|
if not detector_name:
|
||||||
|
return
|
||||||
|
key = (str(issue_code or "").strip(), detector_name)
|
||||||
|
if key in seen:
|
||||||
|
return
|
||||||
|
seen.add(key)
|
||||||
|
candidates.append(
|
||||||
|
{
|
||||||
|
"issue_code": key[0],
|
||||||
|
"detector": detector_name,
|
||||||
|
"severity": severity,
|
||||||
|
"sample_target_id": sample_target_id,
|
||||||
|
"evidence_paths": evidence_paths or [],
|
||||||
|
"source": source_label,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
for target in repair_targets.get("targets") if isinstance(repair_targets.get("targets"), list) else []:
|
for target in repair_targets.get("targets") if isinstance(repair_targets.get("targets"), list) else []:
|
||||||
if not isinstance(target, dict):
|
if not isinstance(target, dict):
|
||||||
continue
|
continue
|
||||||
|
|
@ -6122,24 +6156,42 @@ def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str,
|
||||||
detectors = [f"{issue_code}_detector"]
|
detectors = [f"{issue_code}_detector"]
|
||||||
evidence_paths = detector_evidence_paths_for_target(target)
|
evidence_paths = detector_evidence_paths_for_target(target)
|
||||||
for detector in detectors:
|
for detector in detectors:
|
||||||
key = (issue_code, detector)
|
add_candidate(
|
||||||
if key in seen:
|
issue_code=issue_code,
|
||||||
continue
|
detector=detector,
|
||||||
seen.add(key)
|
severity=target.get("severity"),
|
||||||
candidates.append(
|
sample_target_id=target.get("target_id"),
|
||||||
{
|
evidence_paths=evidence_paths,
|
||||||
"issue_code": issue_code,
|
source_label="repair_target",
|
||||||
"detector": detector,
|
)
|
||||||
"severity": target.get("severity"),
|
declared_codes = normalize_string_list(declared_issue_codes)
|
||||||
"sample_target_id": target.get("target_id"),
|
detector_to_declared_issue: dict[str, str] = {}
|
||||||
"evidence_paths": evidence_paths,
|
for issue_code in declared_codes:
|
||||||
}
|
entry = issues.get(issue_code) if isinstance(issues.get(issue_code), dict) else {}
|
||||||
|
for detector in normalize_string_list(entry.get("detectors")):
|
||||||
|
detector_to_declared_issue.setdefault(detector, issue_code)
|
||||||
|
add_candidate(
|
||||||
|
issue_code=issue_code,
|
||||||
|
detector=detector,
|
||||||
|
severity=entry.get("severity"),
|
||||||
|
sample_target_id="manifest.issue_codes_under_test",
|
||||||
|
source_label="manifest.issue_codes_under_test",
|
||||||
|
)
|
||||||
|
for detector in normalize_string_list(declared_detectors):
|
||||||
|
issue_code = detector_to_declared_issue.get(detector, "")
|
||||||
|
add_candidate(
|
||||||
|
issue_code=issue_code,
|
||||||
|
detector=detector,
|
||||||
|
sample_target_id="manifest.detectors_under_test",
|
||||||
|
source_label="manifest.detectors_under_test",
|
||||||
)
|
)
|
||||||
if should_add_accepted_pack_smoke_detectors(repair_targets, candidates):
|
if should_add_accepted_pack_smoke_detectors(repair_targets, candidates):
|
||||||
candidates.extend(build_accepted_pack_smoke_detector_candidates())
|
candidates.extend(build_accepted_pack_smoke_detector_candidates())
|
||||||
return {
|
return {
|
||||||
"schema_version": "detector_candidates_v1",
|
"schema_version": "detector_candidates_v1",
|
||||||
"candidate_count": len(candidates),
|
"candidate_count": len(candidates),
|
||||||
|
"declared_detector_count": len(normalize_string_list(declared_detectors)),
|
||||||
|
"declared_issue_code_count": len(declared_codes),
|
||||||
"candidates": candidates,
|
"candidates": candidates,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6149,6 +6201,7 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
||||||
return {
|
return {
|
||||||
"status": "not_run",
|
"status": "not_run",
|
||||||
"detector_count": 0,
|
"detector_count": 0,
|
||||||
|
"declared_detector_count": 0,
|
||||||
"pass": 0,
|
"pass": 0,
|
||||||
"fail": 0,
|
"fail": 0,
|
||||||
"review": 0,
|
"review": 0,
|
||||||
|
|
@ -6172,9 +6225,11 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
||||||
return names[:limit]
|
return names[:limit]
|
||||||
|
|
||||||
status = str(summary.get("status") or "skipped")
|
status = str(summary.get("status") or "skipped")
|
||||||
|
declared_detector_count = len(normalize_string_list(detector_results.get("declared_detectors_under_test")))
|
||||||
return {
|
return {
|
||||||
"status": status,
|
"status": status,
|
||||||
"detector_count": int(summary.get("detector_count") or len(results)),
|
"detector_count": int(summary.get("detector_count") or len(results)),
|
||||||
|
"declared_detector_count": declared_detector_count,
|
||||||
"pass": int(summary.get("pass") or 0),
|
"pass": int(summary.get("pass") or 0),
|
||||||
"fail": int(summary.get("fail") or 0),
|
"fail": int(summary.get("fail") or 0),
|
||||||
"review": int(summary.get("review") or 0),
|
"review": int(summary.get("review") or 0),
|
||||||
|
|
@ -6188,6 +6243,12 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
|
||||||
|
|
||||||
def evaluate_detector_results_gate(detector_results_summary: dict[str, Any]) -> tuple[bool, str]:
|
def evaluate_detector_results_gate(detector_results_summary: dict[str, Any]) -> tuple[bool, str]:
|
||||||
status = str(detector_results_summary.get("status") or "not_run").strip()
|
status = str(detector_results_summary.get("status") or "not_run").strip()
|
||||||
|
declared_detector_count = int(detector_results_summary.get("declared_detector_count") or 0)
|
||||||
|
detector_count = int(detector_results_summary.get("detector_count") or 0)
|
||||||
|
if declared_detector_count > 0 and detector_count == 0:
|
||||||
|
return False, "declared_detector_results_missing"
|
||||||
|
if declared_detector_count > 0 and status == "skipped":
|
||||||
|
return False, "declared_detector_results_skipped"
|
||||||
if status == "fail":
|
if status == "fail":
|
||||||
failed = normalize_string_list(detector_results_summary.get("failed_detectors"))
|
failed = normalize_string_list(detector_results_summary.get("failed_detectors"))
|
||||||
suffix = ",".join(failed) if failed else str(detector_results_summary.get("fail") or 0)
|
suffix = ",".join(failed) if failed else str(detector_results_summary.get("fail") or 0)
|
||||||
|
|
@ -6894,6 +6955,7 @@ def build_loop_final_status(loop_state: dict[str, Any]) -> str:
|
||||||
|
|
||||||
def handle_run_pack_loop(args: argparse.Namespace) -> int:
|
def handle_run_pack_loop(args: argparse.Namespace) -> int:
|
||||||
manifest_path = Path(args.manifest).resolve()
|
manifest_path = Path(args.manifest).resolve()
|
||||||
|
pack_manifest = read_json_file(manifest_path)
|
||||||
loop_id = str(args.loop_id or slugify_case_id("domain_pack_loop", None)).strip()
|
loop_id = str(args.loop_id or slugify_case_id("domain_pack_loop", None)).strip()
|
||||||
loop_dir = Path(args.output_root).resolve() / loop_id
|
loop_dir = Path(args.output_root).resolve() / loop_id
|
||||||
iterations_dir = loop_dir / "iterations"
|
iterations_dir = loop_dir / "iterations"
|
||||||
|
|
@ -7026,13 +7088,21 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
|
||||||
"source_repair_targets": repo_relative(repair_targets_path),
|
"source_repair_targets": repo_relative(repair_targets_path),
|
||||||
"items": collect_rerun_matrix(repair_targets),
|
"items": collect_rerun_matrix(repair_targets),
|
||||||
}
|
}
|
||||||
detector_candidates = build_detector_candidates(repair_targets)
|
declared_detectors = normalize_string_list(pack_manifest.get("detectors_under_test"))
|
||||||
|
declared_issue_codes = normalize_string_list(pack_manifest.get("issue_codes_under_test"))
|
||||||
|
detector_candidates = build_detector_candidates(
|
||||||
|
repair_targets,
|
||||||
|
declared_detectors=declared_detectors,
|
||||||
|
declared_issue_codes=declared_issue_codes,
|
||||||
|
)
|
||||||
write_json(detector_candidates_path, detector_candidates)
|
write_json(detector_candidates_path, detector_candidates)
|
||||||
detector_results = agent_detector_runner.build_detector_results(
|
detector_results = agent_detector_runner.build_detector_results(
|
||||||
pack_dir,
|
pack_dir,
|
||||||
detector_candidates_path=detector_candidates_path,
|
detector_candidates_path=detector_candidates_path,
|
||||||
include_default_global=False,
|
include_default_global=False,
|
||||||
)
|
)
|
||||||
|
detector_results["declared_detectors_under_test"] = declared_detectors
|
||||||
|
detector_results["declared_issue_codes_under_test"] = declared_issue_codes
|
||||||
write_json(detector_results_path, detector_results)
|
write_json(detector_results_path, detector_results)
|
||||||
detector_results_summary = summarize_detector_results(detector_results)
|
detector_results_summary = summarize_detector_results(detector_results)
|
||||||
detector_results_gate_ok, detector_results_gate_reason = evaluate_detector_results_gate(detector_results_summary)
|
detector_results_gate_ok, detector_results_gate_reason = evaluate_detector_results_gate(detector_results_summary)
|
||||||
|
|
|
||||||
|
|
@ -1830,6 +1830,16 @@ def build_replay_validation_stage_summary(
|
||||||
"global_plan_refs": stage_manifest.get("global_plan_refs") or base.get("global_plan_refs") or [],
|
"global_plan_refs": stage_manifest.get("global_plan_refs") or base.get("global_plan_refs") or [],
|
||||||
"target_score": stage_manifest.get("target_score", base.get("target_score")),
|
"target_score": stage_manifest.get("target_score", base.get("target_score")),
|
||||||
"acceptance_invariants": stage_manifest.get("acceptance_invariants") or base.get("acceptance_invariants") or [],
|
"acceptance_invariants": stage_manifest.get("acceptance_invariants") or base.get("acceptance_invariants") or [],
|
||||||
|
"loop_final_status": validation.get("final_status") or base.get("loop_final_status"),
|
||||||
|
"stop_reason": "accepted_validation_ingested" if accepted else "validation_not_accepted",
|
||||||
|
"iterations_ran": base.get("iterations_ran") or (1 if validation.get("last_iteration_id") else None),
|
||||||
|
"last_quality_score": validation.get("quality_score", base.get("last_quality_score")),
|
||||||
|
"last_analyst_decision": validation.get("review_overall_status") or base.get("last_analyst_decision"),
|
||||||
|
"last_deterministic_gate_ok": validation.get("deterministic_gate_ok", base.get("last_deterministic_gate_ok")),
|
||||||
|
"last_deterministic_gate_reason": validation.get(
|
||||||
|
"deterministic_gate_reason",
|
||||||
|
base.get("last_deterministic_gate_reason"),
|
||||||
|
),
|
||||||
"accepted_gate": accepted,
|
"accepted_gate": accepted,
|
||||||
"manual_confirmation_required": accepted,
|
"manual_confirmation_required": accepted,
|
||||||
"next_action": next_action,
|
"next_action": next_action,
|
||||||
|
|
@ -2608,6 +2618,10 @@ def build_stage_status(stage_manifest: dict[str, Any], stage_dir: Path) -> dict[
|
||||||
"summary_exists": bool(summary),
|
"summary_exists": bool(summary),
|
||||||
"repair_mode": repair_mode,
|
"repair_mode": repair_mode,
|
||||||
"loop_final_status": summary.get("loop_final_status"),
|
"loop_final_status": summary.get("loop_final_status"),
|
||||||
|
"last_quality_score": summary.get("last_quality_score"),
|
||||||
|
"last_analyst_decision": summary.get("last_analyst_decision"),
|
||||||
|
"last_deterministic_gate_ok": summary.get("last_deterministic_gate_ok"),
|
||||||
|
"last_deterministic_gate_reason": summary.get("last_deterministic_gate_reason"),
|
||||||
"accepted_gate": summary.get("accepted_gate"),
|
"accepted_gate": summary.get("accepted_gate"),
|
||||||
"loop_accepted_gate": summary.get("loop_accepted_gate"),
|
"loop_accepted_gate": summary.get("loop_accepted_gate"),
|
||||||
"stage_closing_gate": stage_closing_gate or None,
|
"stage_closing_gate": stage_closing_gate or None,
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,60 @@ class AgentDetectorRunnerTests(unittest.TestCase):
|
||||||
self.assertEqual(results["summary"]["status"], "pass")
|
self.assertEqual(results["summary"]["status"], "pass")
|
||||||
self.assertEqual(results["results"][0]["status"], "pass")
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
|
|
||||||
|
def test_required_any_detector_can_exclude_selected_object_followup_paths(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
artifact_dir = root / "run"
|
||||||
|
write_text(
|
||||||
|
artifact_dir / "scenarios" / "margin_root" / "steps" / "step_01" / "output.md",
|
||||||
|
"Top margin item: revenue 100, cogs 60, gross profit 40, margin 40%.",
|
||||||
|
)
|
||||||
|
write_text(
|
||||||
|
artifact_dir
|
||||||
|
/ "scenarios"
|
||||||
|
/ "margin_followup"
|
||||||
|
/ "steps"
|
||||||
|
/ "step_03_selected_item_supplier"
|
||||||
|
/ "output.md",
|
||||||
|
"Supplier for the selected item: Example LLC.",
|
||||||
|
)
|
||||||
|
registry_path = root / "detector_registry.json"
|
||||||
|
issue_catalog_path = root / "issue_catalog.json"
|
||||||
|
write_json(
|
||||||
|
registry_path,
|
||||||
|
{
|
||||||
|
"schema_version": "agent_detector_registry_v1",
|
||||||
|
"detectors": {
|
||||||
|
"missing_revenue_cogs_margin_fields": {
|
||||||
|
"kind": "answer_text_required_any",
|
||||||
|
"automation_level": "semi_automatic",
|
||||||
|
"description": "Root margin answers need margin fields.",
|
||||||
|
"issue_codes": ["margin_domain_leak_accounting_route"],
|
||||||
|
"inputs": ["output.md"],
|
||||||
|
"check": {
|
||||||
|
"required_patterns_any": ["(?i)(revenue|cogs|gross profit|margin)"],
|
||||||
|
"artifact_path_exclude_patterns": ["(?i)selected_item"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
write_json(issue_catalog_path, {"schema_version": "agent_issue_catalog_v1", "issues": {}})
|
||||||
|
|
||||||
|
results = runner.build_detector_results(
|
||||||
|
artifact_dir,
|
||||||
|
detector_names=["missing_revenue_cogs_margin_fields"],
|
||||||
|
registry_path=registry_path,
|
||||||
|
issue_catalog_path=issue_catalog_path,
|
||||||
|
include_default_global=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(results["summary"]["status"], "pass")
|
||||||
|
self.assertEqual(results["results"][0]["status"], "pass")
|
||||||
|
evidence_paths = [item["path"] for item in results["results"][0]["evidence"]]
|
||||||
|
self.assertEqual(len(evidence_paths), 1)
|
||||||
|
self.assertIn("margin_root", evidence_paths[0])
|
||||||
|
|
||||||
def test_composite_detector_fails_after_child_detector_fails(self) -> None:
|
def test_composite_detector_fails_after_child_detector_fails(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,57 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
|
||||||
self.assertEqual(gate["detector_results_summary"]["status"], "pass")
|
self.assertEqual(gate["detector_results_summary"]["status"], "pass")
|
||||||
self.assertIn("detector_results_no_repair_signal:pass", gate["blocking_reasons"])
|
self.assertIn("detector_results_no_repair_signal:pass", gate["blocking_reasons"])
|
||||||
|
|
||||||
|
def test_detector_candidates_include_manifest_declared_detectors_without_repair_targets(self) -> None:
|
||||||
|
repair_targets = {
|
||||||
|
"target_count": 0,
|
||||||
|
"acceptance_status": "accepted",
|
||||||
|
"final_status": "accepted",
|
||||||
|
"targets": [],
|
||||||
|
}
|
||||||
|
catalog = {
|
||||||
|
"issues": {
|
||||||
|
"technical_garbage_in_answer": {
|
||||||
|
"severity": "P0",
|
||||||
|
"detectors": ["runtime_tokens_in_user_answer", "capability_ids_in_user_answer"],
|
||||||
|
},
|
||||||
|
"margin_domain_leak_accounting_route": {
|
||||||
|
"severity": "P0",
|
||||||
|
"detectors": ["forbidden_margin_terms"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates = dcl.build_detector_candidates(
|
||||||
|
repair_targets,
|
||||||
|
catalog=catalog,
|
||||||
|
declared_issue_codes=["technical_garbage_in_answer"],
|
||||||
|
declared_detectors=["forbidden_margin_terms"],
|
||||||
|
)
|
||||||
|
detector_names = [item["detector"] for item in candidates["candidates"]]
|
||||||
|
source_by_detector = {item["detector"]: item["source"] for item in candidates["candidates"]}
|
||||||
|
|
||||||
|
self.assertEqual(candidates["declared_detector_count"], 1)
|
||||||
|
self.assertEqual(candidates["declared_issue_code_count"], 1)
|
||||||
|
self.assertIn("runtime_tokens_in_user_answer", detector_names)
|
||||||
|
self.assertIn("capability_ids_in_user_answer", detector_names)
|
||||||
|
self.assertIn("forbidden_margin_terms", detector_names)
|
||||||
|
self.assertEqual(source_by_detector["forbidden_margin_terms"], "manifest.detectors_under_test")
|
||||||
|
|
||||||
|
def test_detector_gate_blocks_empty_run_when_manifest_declares_detectors(self) -> None:
|
||||||
|
summary = dcl.summarize_detector_results(
|
||||||
|
{
|
||||||
|
"summary": {"status": "skipped", "detector_count": 0, "pass": 0, "fail": 0, "review": 0, "skipped": 0},
|
||||||
|
"results": [],
|
||||||
|
"declared_detectors_under_test": ["forbidden_margin_terms"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ok, reason = dcl.evaluate_detector_results_gate(summary)
|
||||||
|
|
||||||
|
self.assertFalse(ok)
|
||||||
|
self.assertEqual(summary["declared_detector_count"], 1)
|
||||||
|
self.assertEqual(reason, "declared_detector_results_missing")
|
||||||
|
|
||||||
def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None:
|
def test_auto_coder_gate_allows_when_detector_results_confirm_failure(self) -> None:
|
||||||
repair_targets = {
|
repair_targets = {
|
||||||
"targets": [
|
"targets": [
|
||||||
|
|
|
||||||
|
|
@ -634,6 +634,13 @@ class StageAgentLoopTests(unittest.TestCase):
|
||||||
self.assertTrue(validation["accepted_after_repair"])
|
self.assertTrue(validation["accepted_after_repair"])
|
||||||
self.assertEqual(validation["quality_score"], 92)
|
self.assertEqual(validation["quality_score"], 92)
|
||||||
self.assertEqual(validation["pack_final_status"], "partial")
|
self.assertEqual(validation["pack_final_status"], "partial")
|
||||||
|
self.assertEqual(summary["last_quality_score"], 92)
|
||||||
|
self.assertTrue(summary["last_deterministic_gate_ok"])
|
||||||
|
self.assertEqual(summary["last_deterministic_gate_reason"], "deterministic_gate_passed")
|
||||||
|
self.assertEqual(status["last_quality_score"], 92)
|
||||||
|
self.assertEqual(status["last_analyst_decision"], "accepted")
|
||||||
|
self.assertTrue(status["last_deterministic_gate_ok"])
|
||||||
|
self.assertEqual(status["last_deterministic_gate_reason"], "deterministic_gate_passed")
|
||||||
self.assertEqual(status["latest_validation_source"], "domain_case_loop.run-pack-loop")
|
self.assertEqual(status["latest_validation_source"], "domain_case_loop.run-pack-loop")
|
||||||
self.assertEqual(status["latest_replay_validation_status"], "passed")
|
self.assertEqual(status["latest_replay_validation_status"], "passed")
|
||||||
self.assertTrue(status["automation_followup"]["ready_for_next_domain_slice"])
|
self.assertTrue(status["automation_followup"]["ready_for_next_domain_slice"])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue