Files
whetstone_DSL/tools/mcp/check_native_decomposition_impact_coverage.py

173 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
from typing import Dict, List, Tuple
def load_json(path: Path) -> Dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def write_json(path: Path, obj: Dict) -> None:
with path.open("w", encoding="utf-8") as f:
json.dump(obj, f, indent=2, sort_keys=True)
f.write("\n")
def load_native_tasks(run_dir: Path, summary: Dict) -> List[Dict]:
retry = summary.get("native_decomposition_retry") or {}
retry_applied = bool(retry.get("applied", False))
retry_path = run_dir / "02aa_generate_taskitems_retry.json"
base_path = run_dir / "02_generate_taskitems.json"
tasks: List[Dict] = []
if retry_applied and retry_path.exists():
data = load_json(retry_path)
if isinstance(data, dict):
tasks = list(data.get("tasks") or [])
elif base_path.exists():
data = load_json(base_path)
if isinstance(data, dict):
tasks = list(data.get("tasks") or [])
# Include explicitly injected extra tasks when present.
extra = summary.get("extra_tasks") or []
if isinstance(extra, list) and extra:
tasks = tasks + extra
return tasks
def load_spec_text(summary: Dict, repo_root: Path) -> str:
input_file = str(summary.get("input_file", "")).strip()
if not input_file:
return ""
p = Path(input_file)
if not p.is_absolute():
p = repo_root / input_file
try:
return p.read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
def activate_profiles(profiles: List[Dict], spec_text: str) -> List[Dict]:
s = spec_text.lower()
active = []
for profile in profiles:
keys = [str(k).lower() for k in (profile.get("trigger_keywords") or [])]
if not keys:
continue
if any(k in s for k in keys):
active.append(profile)
return active
def collect_task_unions(tasks: List[Dict]) -> Tuple[List[str], List[str], List[Dict]]:
reasons: List[str] = []
prereq_ops: List[str] = []
contracts: List[Dict] = []
for t in tasks:
rs = t.get("reasons") or []
po = t.get("prerequisiteOps") or []
ec = t.get("executionContract") or {}
for r in rs:
reasons.append(str(r).lower())
for op in po:
prereq_ops.append(str(op))
if isinstance(ec, dict):
contracts.append(ec)
return reasons, prereq_ops, contracts
def check_profile(profile: Dict, tasks: List[Dict]) -> Dict:
reasons, prereq_ops, contracts = collect_task_unions(tasks)
missing: List[str] = []
min_tasks = int(profile.get("min_native_task_count", 0) or 0)
if len(tasks) < min_tasks:
missing.append(f"native_task_count<{min_tasks}")
req_reason_keys = [str(x).lower() for x in (profile.get("required_reason_keywords") or [])]
for key in req_reason_keys:
if not any(key in r for r in reasons):
missing.append(f"missing_reason_keyword:{key}")
req_ops = [str(x) for x in (profile.get("required_prerequisite_ops") or [])]
ops_set = set(prereq_ops)
for op in req_ops:
if op not in ops_set:
missing.append(f"missing_prerequisite_op:{op}")
req_contract = [str(x) for x in (profile.get("required_execution_contract") or [])]
for field in req_contract:
has_field_true = False
for ec in contracts:
if isinstance(ec.get(field), bool):
if ec.get(field) is True:
has_field_true = True
break
elif field in ec:
has_field_true = True
break
if not has_field_true:
missing.append(f"missing_execution_contract:{field}")
return {
"id": profile.get("id", "unknown"),
"description": profile.get("description", ""),
"passed": len(missing) == 0,
"native_task_count": len(tasks),
"missing": missing,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Check native decomposition coverage against impact profiles.")
parser.add_argument("--run-dir", required=True, help="Pipeline run directory containing 00_summary.json")
parser.add_argument("--profiles", default="tools/mcp/profiles/native_decomposition_impact_profiles.json", help="Profile JSON path")
parser.add_argument("--out", required=True, help="Output report path")
parser.add_argument("--enforce", action="store_true", help="Exit nonzero on failures")
args = parser.parse_args()
run_dir = Path(args.run_dir)
summary_path = run_dir / "00_summary.json"
if not summary_path.exists():
raise SystemExit(f"missing summary: {summary_path}")
summary = load_json(summary_path)
profiles_doc = load_json(Path(args.profiles))
profiles = list(profiles_doc.get("profiles") or [])
repo_root = Path(__file__).resolve().parents[2]
spec_text = load_spec_text(summary, repo_root)
active = activate_profiles(profiles, spec_text)
tasks = load_native_tasks(run_dir, summary)
checks = [check_profile(p, tasks) for p in active]
failing = [c for c in checks if not c.get("passed", False)]
result = {
"status": "ok" if not failing else "fail",
"run_dir": str(run_dir),
"active_profile_count": len(active),
"failing_profile_count": len(failing),
"active_profiles": [p.get("id", "unknown") for p in active],
"checks": checks,
}
write_json(Path(args.out), result)
print(json.dumps({
"status": result["status"],
"active_profile_count": result["active_profile_count"],
"failing_profile_count": result["failing_profile_count"],
"out": args.out,
}, sort_keys=True))
if args.enforce and failing:
return 12
return 0
if __name__ == "__main__":
raise SystemExit(main())