Files
FreeCADWasmDev/tools/record_runtime_issue.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

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())