102 lines
4.7 KiB
Python
102 lines
4.7 KiB
Python
#!/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())
|