#!/usr/bin/env python3 """Record one observed Web capability failure and wake the focused planner.""" from __future__ import annotations import argparse import hashlib import json from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] CAPABILITIES = ROOT / "docs/current/feature-capability-registry.json" ISSUES = ROOT / "docs/current/runtime-issue-ledger.json" ARTIFACTS = ROOT / "web/public/modules/manifest.json" SOURCE_INVENTORY = ROOT / "docs/format-api-inventory.json" ACTIVE_STATUSES = {"open", "confirmed", "reopened", "fix_deployed_pending_use"} def observation(issue: dict) -> str: fields = {key: issue.get(key) for key in ("id", "capabilityId", "args", "firstError", "failureKeys", "lastSeenAt", "occurrences")} return hashlib.sha256(json.dumps(fields, sort_keys=True, ensure_ascii=False).encode()).hexdigest() def read(path: Path) -> dict: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} return value if isinstance(value, dict) else {} def runtime_capabilities(root: Path) -> dict[str, dict]: rows = read(root / "docs/current/feature-capability-registry.json").get("records", []) capabilities = {row["id"]: row for row in rows if isinstance(row, dict) and row.get("id")} for row in read(root / "docs/current/product-business-backlog.json").get("records", []): if isinstance(row, dict) and str(row.get("id", "")).startswith("product:"): capabilities[row["id"]] = {**row, "module": "Web", "command": row.get("name"), "sourcePath": row.get("source")} return capabilities def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("capability_id", help="registered Module:command identifier") parser.add_argument("--args-json", default="{}", help="observed command args as JSON") parser.add_argument("--error", required=True, help="first observed error or incorrect behavior") parser.add_argument("--request-id") parser.add_argument("--check-only", action="store_true", help="validate the observed business ID and args without writing an issue") parser.add_argument("--failure-key", action="append", default=[], help="historical failure addressed by this business issue") args = parser.parse_args() capability = runtime_capabilities(ROOT).get(args.capability_id) if capability is None: raise SystemExit(f"unknown capability: {args.capability_id}") try: command_args = json.loads(args.args_json) except json.JSONDecodeError as error: raise SystemExit(f"--args-json is invalid: {error}") from error module = str(capability.get("module")) if args.check_only: print(json.dumps({"status": "valid", "capabilityId": args.capability_id, "module": module, "args": command_args})) return 0 artifact = read(ARTIFACTS).get("modules", {}).get(module, {}) source_revision = read(SOURCE_INVENTORY).get("source_revision", "unknown") identity = json.dumps([args.capability_id, command_args, args.error], ensure_ascii=False, sort_keys=True) issue_id = "runtime:" + hashlib.sha256(identity.encode("utf-8")).hexdigest()[:20] ledger = read(ISSUES) or {"schema": "freecad-web-runtime-issue-ledger/v1", "mode": "fix-on-use", "issues": []} issues = ledger.setdefault("issues", []) now = datetime.now(timezone.utc).isoformat() current = next((row for row in issues if isinstance(row, dict) and row.get("id") == issue_id), None) payload = { "id": issue_id, "status": "reopened" if current else "open", "capabilityId": args.capability_id, "module": module, "cmd": capability.get("command"), "args": command_args, "requestId": args.request_id, "firstError": args.error, "failureKeys": sorted(set(args.failure_key) | set(current.get("failureKeys", []) if current else [])), "sourceRevision": source_revision, "artifact": artifact, "firstSeenAt": current.get("firstSeenAt", now) if current else now, "lastSeenAt": now, "occurrences": int(current.get("occurrences", 0) or 0) + 1 if current else 1, } if current: current.clear() current.update(payload) else: issues.append(payload) ledger["updatedAt"] = now temporary = ISSUES.with_suffix(".tmp") temporary.write_text(json.dumps(ledger, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(ISSUES) print(json.dumps({"status": payload["status"], "issueId": issue_id, "capabilityId": args.capability_id}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())