Files
Kill_LIFE/tools/mcp_runtime_status.py
T
root e67fb754c2 feat: KiCad 10 native, QEMU simulation, compliance EU, firmware fixes
Session kxkm-ai 25-26 mars 2026 — 98/98 tâches TODO complétées.

Hardware:
- KiCad 10.0.0 native avec sym/fp-lib-table locales
- Nouveau block SPI (gen_spi_header.py), ERC clean
- Module partagé hardware/lib/kicad_gen.py (5 générateurs refactorisés)
- Pipeline export: tools/hw/hw_export.sh (ERC + SVG + PDF + netlist)
- KiBot 1.8.5 installé, .kibot.yaml configuré

Firmware:
- Fix I2S driver conflict: migration I2sMic vers nouvelle API i2s_channel_*
- Fix WDT risk: yield() dans CompletePushToTalk (voice_controller.cpp)
- Fix XSS wifi_manager.cpp: innerHTML → createElement/textContent
- Fix null check FwIsValidWavHeader
- Dead code supprimé: i2s_audio.cpp.bak, i2s_audio.h

Simulation MCU:
- QEMU ESP32-S3 v9.2.2 installé (tools/sim/)
- Script run_qemu_esp32s3.sh — boot OK vérifié
- [env:esp32s3_qemu] dans platformio.ini
- Wokwi CI: wokwi.toml + diagram.json + scenario.yaml
- SPICE bridge POC: tools/sim/spice_bridge.py (ngspice → ADC/brownout)

Compliance:
- Profil iot_wifi_eu validé (16 standards, 8 evidence)
- 4 evidence remplis: risk_assessment, security_architecture,
  test_plan_radio_emc, supply_chain_declarations
- plan.yaml complété avec données produit réelles

CI:
- Job hardware-export (KiCad 10 ERC + SVG + PDF + netlist)
- Job firmware-sim (Wokwi, conditionnel WOKWI_CLI_TOKEN)
- evidence_pack.yml enrichi avec exports hardware

Docs & RAG:
- specs/02_arch.md complet (481 lignes, 4 ADR, diagrammes)
- docs/SIMULATION.md (3 niveaux: native, QEMU, Wokwi)
- 6 chunks ingérés dans kb-kicad RAG
- Dataset HF: tools/generate_hf_dataset.py + datasets/kb_kicad_qa.jsonl
- Rapport analyse: docs/plans/ANALYSIS_REPORT_2026-03-25.md
- Recherche OSS: docs/research/oss_similar_projects.md

Infra:
- ZeroClaw 0.1.7 installé (cargo install)
- APIFY_API_KEY configurée, smoke OK
- Tests: 39/39 firmware + 26/26 Python stable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:52:00 +01:00

264 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Aggregate local MCP runtime readiness across Kill_LIFE smoke helpers."""
from __future__ import annotations
import asyncio
import argparse
import json
import sys
from pathlib import Path
from typing import Any
try:
from .mcp_smoke_common import load_runtime_env
except ImportError: # pragma: no cover - script entrypoint fallback
from mcp_smoke_common import load_runtime_env
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CHECK_TIMEOUT_S = 120.0
DEFAULT_MAX_PARALLEL = 4
CHECKS: tuple[dict[str, Any], ...] = (
{
"name": "validate-specs",
"cmd": ["python3", "tools/validate_specs_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": False,
"timeout_s": 45.0,
},
{
"name": "kicad",
"cmd": ["python3", "tools/hw/mcp_smoke.py", "--json", "--quick", "--timeout", "30"],
"accept_degraded": False,
"timeout_s": 90.0,
},
{
"name": "kicad-host",
"cmd": ["python3", "tools/hw/kicad_host_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": True,
"optional_degraded": True,
"task": "K-012",
"blocked_when": "host_pcbnew_import != ok (optional host-native path)",
"timeout_s": 60.0,
},
{
"name": "knowledge-base",
"cmd": ["python3", "tools/knowledge_base_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": True,
"timeout_s": 60.0,
},
{
"name": "github-dispatch",
"cmd": ["python3", "tools/github_dispatch_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": True,
"timeout_s": 60.0,
},
{
"name": "freecad",
"cmd": ["python3", "tools/freecad_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": False,
"task": "F-101",
"timeout_s": 120.0,
},
{
"name": "openscad",
"cmd": ["python3", "tools/openscad_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": False,
"task": "O-101",
"timeout_s": 120.0,
},
{
"name": "nexar-api",
"cmd": ["python3", "tools/nexar_mcp_smoke.py", "--json"],
"accept_degraded": True,
"task": "K-014",
"blocked_when": "live Nexar unavailable (token missing, demo mode, or external account/quota limit)",
"optional_degraded_when_live_validation": ("quota_exceeded",),
"timeout_s": 120.0,
},
{
"name": "ngspice",
"cmd": ["python3", "tools/ngspice_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": False,
"timeout_s": 60.0,
},
{
"name": "platformio",
"cmd": ["python3", "tools/platformio_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": False,
"timeout_s": 90.0,
},
{
"name": "apify",
"cmd": ["python3", "tools/apify_mcp_smoke.py", "--json", "--quick"],
"accept_degraded": True,
"blocked_when": "APIFY_API_KEY not configured (direct-scrape fallback active)",
"timeout_s": 60.0,
},
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--json", action="store_true")
parser.add_argument("--strict", action="store_true", help="Treat degraded checks as failures.")
parser.add_argument(
"--max-parallel",
type=int,
default=DEFAULT_MAX_PARALLEL,
help=f"Maximum concurrent checks (default: {DEFAULT_MAX_PARALLEL}).",
)
return parser.parse_args()
def classify_overall(results: list[dict[str, Any]], *, strict: bool) -> str:
any_failed = False
any_degraded = False
for result in results:
status = result.get("status")
if status == "failed":
any_failed = True
continue
if status == "degraded" and (strict or not result.get("accept_degraded", False)):
any_failed = True
continue
if status == "degraded" and result.get("optional_degraded", False):
continue
if status == "degraded":
any_degraded = True
if any_failed:
return "failed"
if any_degraded:
return "degraded"
return "ready"
def derive_blockers(results: list[dict[str, Any]]) -> list[dict[str, str]]:
blockers: list[dict[str, str]] = []
for result in results:
task = result.get("task")
if not task or result.get("status") != "degraded" or result.get("optional_degraded", False):
continue
name = result.get("name", "unknown")
error = str(result.get("error") or "blocked by environment")
blocked_when = str(result.get("blocked_when") or "")
blockers.append(
{
"task": task,
"check": name,
"reason": error,
"condition": blocked_when,
}
)
return blockers
async def run_check(spec: dict[str, Any]) -> dict[str, Any]:
timeout_s = float(spec.get("timeout_s") or DEFAULT_CHECK_TIMEOUT_S)
try:
proc = await asyncio.create_subprocess_exec(
*spec["cmd"],
cwd=str(ROOT),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except Exception as exc:
return {
"name": spec["name"],
"status": "failed",
"error": f"spawn failed: {exc}",
"accept_degraded": spec.get("accept_degraded", False),
"optional_degraded": spec.get("optional_degraded", False),
"exit_code": -1,
}
try:
raw_stdout, raw_stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
except TimeoutError:
proc.kill()
await proc.communicate()
payload = {
"status": "failed",
"error": f"timeout after {timeout_s:.1f}s",
"stdout": "",
}
stderr = ""
else:
stdout = raw_stdout.decode("utf-8", errors="replace").strip()
stderr = raw_stderr.decode("utf-8", errors="replace").strip()
try:
payload = json.loads(stdout) if stdout else {}
except json.JSONDecodeError as exc:
payload = {
"status": "failed",
"error": f"invalid JSON: {exc}",
"stdout": stdout,
}
payload.setdefault("status", "failed")
payload.setdefault("error", None)
payload["name"] = spec["name"]
payload["accept_degraded"] = spec.get("accept_degraded", False)
payload["optional_degraded"] = spec.get("optional_degraded", False)
live_validation = payload.get("live_validation")
if (
payload.get("status") == "degraded"
and live_validation in spec.get("optional_degraded_when_live_validation", ())
):
payload["optional_degraded"] = True
if "task" in spec:
payload["task"] = spec["task"]
if "blocked_when" in spec:
payload["blocked_when"] = spec["blocked_when"]
payload["exit_code"] = proc.returncode
if stderr:
payload["stderr"] = stderr
return payload
async def run_all_checks(*, max_parallel: int) -> list[dict[str, Any]]:
limit = max(1, int(max_parallel))
semaphore = asyncio.Semaphore(limit)
async def _guarded(spec: dict[str, Any]) -> dict[str, Any]:
async with semaphore:
return await run_check(spec)
return list(await asyncio.gather(*(_guarded(spec) for spec in CHECKS)))
def emit(payload: dict[str, Any], *, json_output: bool) -> int:
if json_output:
print(json.dumps(payload, ensure_ascii=True))
else:
print(f"MCP runtime status: {payload['status']}")
print(f"Checks: {payload['counts']['ready']} ready / {payload['counts']['degraded']} degraded / {payload['counts']['failed']} failed")
if payload["blockers"]:
print("Open blocked tasks:")
for blocker in payload["blockers"]:
print(f"- {blocker['task']} via {blocker['check']}: {blocker['reason']}")
return 0 if payload["status"] == "ready" else 1
def main() -> int:
args = parse_args()
load_runtime_env()
results = asyncio.run(run_all_checks(max_parallel=args.max_parallel))
payload = {
"status": classify_overall(results, strict=args.strict),
"strict": args.strict,
"max_parallel": max(1, int(args.max_parallel)),
"checks": results,
"counts": {
"ready": sum(1 for result in results if result.get("status") == "ready"),
"degraded": sum(1 for result in results if result.get("status") == "degraded"),
"failed": sum(1 for result in results if result.get("status") == "failed"),
},
"blockers": derive_blockers(results),
}
return emit(payload, json_output=args.json)
if __name__ == "__main__":
raise SystemExit(main())