@buaa_smat/hometrans 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal platform context query wrapper.
3
+
4
+ The public contract is provider-neutral:
5
+ PlatformContextRequest -> platform-context-result.json
6
+
7
+ Provider details are internal. The script currently supports a DeepWiki-backed
8
+ provider by compiling a small query pack and querying the provider internally.
9
+ It does not summarize evidence or make decisions.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import datetime as dt
16
+ import ssl
17
+ import json
18
+ import random
19
+ import re
20
+ import shutil
21
+ import sys
22
+ import time
23
+ import urllib.error
24
+ import urllib.request
25
+ from concurrent.futures import ThreadPoolExecutor, as_completed
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+
30
+ DEFAULT_API_LEVEL = 22
31
+ ALLOWED_STAGES = {"planner", "coder"}
32
+ DEFAULT_QUERY_TYPES = ("api", "pattern")
33
+ DEEPWIKI_URL = "https://mcp.deepwiki.com/mcp"
34
+ READ_TIMEOUT_SECONDS = 120
35
+ MAX_RESPONSE_CHARS = 1600
36
+ MAX_WORKERS = 2
37
+ MAX_RETRIES = 2
38
+ RETRY_BASE_SECONDS = 2.0
39
+ DEEPWIKI_REPO = "jjjzzz666/dev_kb_repo"
40
+ PROVIDER_FAILURE_PATTERNS = (
41
+ "Error processing question",
42
+ "Query failed",
43
+ "Error while executing query",
44
+ )
45
+ PLANNER_SLOTS = (
46
+ "feasibility",
47
+ "owner_boundary",
48
+ "forbidden_path",
49
+ "version_gate",
50
+ "protocol_variant",
51
+ "fallback_gap",
52
+ "blocking_unknown",
53
+ "completion_evidence",
54
+ "implementation_implication",
55
+ )
56
+ CODER_SLOTS = (
57
+ "exact_api",
58
+ "exact_import",
59
+ "return_error",
60
+ "context_permission",
61
+ "lifecycle_threading",
62
+ "local_forbidden",
63
+ "local_validation",
64
+ "planned_fallback",
65
+ )
66
+ ALLOWED_STRUCTURED_SLOTS = frozenset(PLANNER_SLOTS + CODER_SLOTS)
67
+ PREFACE_PATTERNS = (
68
+ "you are asking",
69
+ "you're asking",
70
+ "i understand",
71
+ "based on",
72
+ "summary:",
73
+ "here are",
74
+ )
75
+ STRUCTURED_LINE_RE = re.compile(
76
+ r"^[-*]\s*(?:\*\*)?(?P<slot>[a-z][a-z0-9_]*|extra:[a-z0-9_-]+)(?:\*\*)?\s*:\s*(?P<fact>.+)$",
77
+ re.I,
78
+ )
79
+
80
+
81
+ def load_json(path: Path) -> dict[str, Any]:
82
+ try:
83
+ data = json.loads(path.read_text(encoding="utf-8"))
84
+ except json.JSONDecodeError as exc:
85
+ raise SystemExit(f"{path}: invalid JSON: {exc}") from exc
86
+ if not isinstance(data, dict):
87
+ raise SystemExit("request must be a JSON object")
88
+ return data
89
+
90
+
91
+ def write_json(path: Path, data: Any) -> None:
92
+ path.parent.mkdir(parents=True, exist_ok=True)
93
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
94
+
95
+
96
+ def normalize(text: Any) -> str:
97
+ return re.sub(r"\s+", " ", str(text or "").strip())
98
+
99
+
100
+ def normalize_list(value: Any, name: str) -> list[str]:
101
+ if value is None:
102
+ return []
103
+ if not isinstance(value, list):
104
+ raise SystemExit(f"{name} must be a list")
105
+ return [normalize(item) for item in value if normalize(item)]
106
+
107
+
108
+ def resolve_api_level(request: dict[str, Any], cli_api_level: int | None) -> tuple[int, str]:
109
+ if cli_api_level is not None:
110
+ return cli_api_level, "cli"
111
+ raw = request.get("api_level", request.get("api_version"))
112
+ if raw is not None:
113
+ try:
114
+ return int(raw), "request"
115
+ except (TypeError, ValueError) as exc:
116
+ raise SystemExit("api_level must be an integer") from exc
117
+ return DEFAULT_API_LEVEL, "default"
118
+
119
+
120
+ def validate_request(request: dict[str, Any], api_level: int) -> dict[str, Any]:
121
+ stage = normalize(request.get("stage"))
122
+ if stage not in ALLOWED_STAGES:
123
+ raise SystemExit("stage must be 'planner' or 'coder'")
124
+
125
+ focus_point = normalize(request.get("focus_point"))
126
+ task_excerpt = normalize(request.get("task_excerpt", request.get("spec_excerpt")))
127
+ if not focus_point:
128
+ raise SystemExit("focus_point is required")
129
+ if not task_excerpt:
130
+ raise SystemExit("task_excerpt is required")
131
+
132
+ project_evidence = normalize(request.get("project_evidence")) or "none"
133
+ platform_surfaces = normalize_list(request.get("platform_surfaces"), "platform_surfaces")
134
+ extra_constraints = normalize_list(request.get("extra_constraints"), "extra_constraints")
135
+ return {
136
+ "stage": stage,
137
+ "api_level": api_level,
138
+ "focus_point": focus_point,
139
+ "task_excerpt": task_excerpt,
140
+ "project_evidence": project_evidence,
141
+ "platform_surfaces": platform_surfaces,
142
+ "extra_constraints": extra_constraints,
143
+ "query_types": list(DEFAULT_QUERY_TYPES),
144
+ }
145
+
146
+
147
+ def short_module_name(stage: str, focus_point: str) -> str:
148
+ base = re.sub(r"[^a-zA-Z0-9]+", "_", focus_point).strip("_").lower()
149
+ if not base:
150
+ base = "platform_context"
151
+ return f"{stage}_{base[:72].rstrip('_')}"
152
+
153
+
154
+ def join_items(items: list[str]) -> str:
155
+ return "; ".join(items) if items else "none"
156
+
157
+
158
+ def default_extra_constraints(stage: str) -> list[str]:
159
+ if stage == "planner":
160
+ return [
161
+ "Return only facts that affect feasibility, owner boundary, forbidden path, fallback/gap, edit boundary, blocking unknown, or completion evidence.",
162
+ "Check API-level gates, capability/permission/signing gates, owner/context/lifecycle constraints, sync/async protocol differences, state/persistence timing, return/error/null/default semantics, forbidden paths, false friends, fallback/gaps, completion evidence, and same-capability different-protocol traps.",
163
+ "Use the provided platform surfaces as natural signals. Do not require exact API names from the caller, and do not assume the obvious API family is the only valid protocol.",
164
+ ]
165
+ return [
166
+ "Return only facts that affect exact API/import/type/signature/member availability, return/error/null/default/conflict behavior, context/permission/config, lifecycle/threading, local forbidden usage, planned fallback, or local validation.",
167
+ "Check exact module imports, types, members, signatures, API-level gates, capability/permission/signing gates, owner/context/lifecycle constraints, sync/async protocol differences, state/persistence timing, return/error/null/default/conflict semantics, forbidden paths, false friends, and compile/runtime traps.",
168
+ "Use the provided platform surfaces as natural signals. Keep the answer local to the approved implementation point; do not redesign the architecture.",
169
+ ]
170
+
171
+
172
+ def build_stage_checks(stage: str, query_type: str) -> str:
173
+ if stage == "planner":
174
+ if query_type == "api":
175
+ return (
176
+ "path feasibility at this API level; required owner/context/lifecycle/"
177
+ "capability gate; API family traps that can invalidate the plan; "
178
+ "fallback or blocking gap; code-observable completion evidence affected by this fact"
179
+ )
180
+ if query_type == "migration":
181
+ return (
182
+ "cross-platform assumptions that would make the plan invalid; "
183
+ "HarmonyOS primary path; forbidden migration paths; owner/lifecycle/"
184
+ "permission differences; fallback or blocking gap"
185
+ )
186
+ return (
187
+ "forbidden paths and false friends; negative space that looks valid but "
188
+ "should not be used; valid high-level pattern only if it changes the plan; "
189
+ "hidden owner/lifecycle/state pitfalls; fallback or blocking gap; "
190
+ "the smallest evidence a planner can require to know the path is complete"
191
+ )
192
+
193
+ if query_type == "api":
194
+ return (
195
+ "exact import/module/type/member/signature and minimal call shape; API-level "
196
+ "availability for this call; required permission/config/context; return/error/"
197
+ "null/default/conflict semantics; call-site lifecycle/threading constraints; "
198
+ "compile/runtime failure traps"
199
+ )
200
+ if query_type == "migration":
201
+ return (
202
+ "old-platform assumptions that would break this local edit; exact HarmonyOS "
203
+ "replacement API or local pattern; incompatible parameters/return semantics; "
204
+ "permission/lifecycle differences at this call site"
205
+ )
206
+ return (
207
+ "minimal valid local usage pattern; local forbidden usage that would fail "
208
+ "compile/runtime; lifecycle/permission/state pitfalls at this call site; "
209
+ "whether the planned fallback is valid; minimal validation checks for this edit; "
210
+ "do not redesign the architecture"
211
+ )
212
+
213
+
214
+ def stage_quality_bar(stage: str) -> str:
215
+ if stage == "planner":
216
+ return (
217
+ "Quality bar: a planner can write a short contract from the answer: "
218
+ "main path, forbidden path, fallback/gap, owner/lifecycle boundary, "
219
+ "blocking unknown if unresolved, and observable completion evidence. "
220
+ )
221
+ return (
222
+ "Quality bar: a coder can write or check the local code from the answer: "
223
+ "exact import/module/type/member/signature, minimal call shape, context/"
224
+ "permission/config, lifecycle/threading, return/error/null/default/conflict "
225
+ "semantics, forbidden local usage, and validation check. "
226
+ )
227
+
228
+
229
+ def build_query(request: dict[str, Any], query_type: str) -> str:
230
+ stage_goal = (
231
+ "choose a valid implementation path before coding"
232
+ if request["stage"] == "planner"
233
+ else "verify exact local API usage while implementing"
234
+ )
235
+ surfaces = join_items(request["platform_surfaces"])
236
+ constraints = join_items(request["extra_constraints"] or default_extra_constraints(request["stage"]))
237
+ checks = build_stage_checks(request["stage"], query_type)
238
+ slots = ", ".join(PLANNER_SLOTS if request["stage"] == "planner" else CODER_SLOTS)
239
+
240
+ return (
241
+ f"HarmonyOS ArkTS/ArkUI platform fact check. API level: {request['api_level']}. "
242
+ f"Stage goal: {stage_goal}. Focus: {request['focus_point']}. "
243
+ f"Task constraint: {request['task_excerpt']}. "
244
+ f"Project evidence: {request['project_evidence']}. "
245
+ f"Platform surfaces: {surfaces}. Extra constraints: {constraints}. "
246
+ f"Check only: {checks}. "
247
+ f"{stage_quality_bar(request['stage'])}"
248
+ "Return concise evidence bullets only. "
249
+ f"Use these slots when applicable: {slots}. "
250
+ "For critical slots, write `slot: none` when the fact is not applicable or not known. "
251
+ "If sources disagree or the fact is unavailable, state the conflict or unknown explicitly; do not smooth it over. "
252
+ "You may add extra:<name> if a critical fact does not fit. "
253
+ "Do not invent missing facts. "
254
+ "Max 8 bullets. Format each bullet as '- slot: fact'. "
255
+ "No preface, headings, examples, tutorials, or steps."
256
+ )
257
+
258
+
259
+ def clean_evidence_response(text: str) -> tuple[str, bool]:
260
+ lines = [line.strip() for line in text.splitlines()]
261
+ structured_lines: list[str] = []
262
+ fallback_lines: list[str] = []
263
+ for line in lines:
264
+ if not line:
265
+ continue
266
+ lowered = line.lower().lstrip("#*-0123456789. ")
267
+ if line.startswith("#") or any(lowered.startswith(pattern) for pattern in PREFACE_PATTERNS):
268
+ continue
269
+ match = STRUCTURED_LINE_RE.match(line)
270
+ if match:
271
+ slot = match.group("slot").lower()
272
+ fact = match.group("fact").strip()
273
+ if not fact:
274
+ continue
275
+ if slot not in ALLOWED_STRUCTURED_SLOTS and not slot.startswith("extra:"):
276
+ slot = f"extra:{slot}"
277
+ structured_lines.append(f"- {slot}: {fact}")
278
+ continue
279
+ fallback_lines.append(line)
280
+ if structured_lines:
281
+ return "\n".join(structured_lines), True
282
+ cleaned = "\n".join(fallback_lines).strip()
283
+ return cleaned or text, False
284
+
285
+
286
+ def build_queries(request: dict[str, Any]) -> list[dict[str, str]]:
287
+ module = short_module_name(request["stage"], request["focus_point"])
288
+ return [
289
+ {
290
+ "module": module,
291
+ "type": query_type,
292
+ "query": build_query(request, query_type),
293
+ }
294
+ for query_type in request["query_types"]
295
+ ]
296
+
297
+
298
+ def read_cache_entries(path: Path) -> list[dict[str, Any]]:
299
+ if not path.exists():
300
+ return []
301
+ entries: list[dict[str, Any]] = []
302
+ for line in path.read_text(encoding="utf-8").splitlines():
303
+ if not line.strip():
304
+ continue
305
+ try:
306
+ entry = json.loads(line)
307
+ except json.JSONDecodeError:
308
+ continue
309
+ if isinstance(entry, dict):
310
+ entries.append(entry)
311
+ return entries
312
+
313
+
314
+ def parse_sse_response(content: str) -> str:
315
+ result = ""
316
+ for raw_line in content.splitlines():
317
+ line = raw_line.strip()
318
+ if not line.startswith("data: "):
319
+ continue
320
+ try:
321
+ data = json.loads(line[6:])
322
+ except json.JSONDecodeError:
323
+ continue
324
+ if "error" in data:
325
+ return f"[provider_error] {data['error']}"
326
+ for item in data.get("result", {}).get("content", []):
327
+ if item.get("type") == "text":
328
+ result += item.get("text", "")
329
+ return result.strip()
330
+
331
+
332
+ def truncate_response(text: str) -> str:
333
+ if len(text) <= MAX_RESPONSE_CHARS:
334
+ return text
335
+ cut = text.rfind("\n\n", 0, MAX_RESPONSE_CHARS)
336
+ if cut < MAX_RESPONSE_CHARS * 0.5:
337
+ cut = MAX_RESPONSE_CHARS
338
+ return text[:cut].rstrip() + "\n[truncated]"
339
+
340
+
341
+ def provider_text_failure(text: str) -> bool:
342
+ return any(pattern in text for pattern in PROVIDER_FAILURE_PATTERNS)
343
+
344
+
345
+ def retry_wait_seconds(attempt: int) -> float:
346
+ base = RETRY_BASE_SECONDS * (2 ** attempt)
347
+ return base * (0.8 + 0.4 * random.random())
348
+
349
+
350
+ def ask_deepwiki_once(repo: str, question: str) -> tuple[bool, str, bool]:
351
+ payload = {
352
+ "jsonrpc": "2.0",
353
+ "method": "tools/call",
354
+ "id": 1,
355
+ "params": {
356
+ "name": "ask_question",
357
+ "arguments": {
358
+ "repoName": repo,
359
+ "question": question,
360
+ },
361
+ },
362
+ }
363
+ request = urllib.request.Request(
364
+ DEEPWIKI_URL,
365
+ data=json.dumps(payload).encode("utf-8"),
366
+ headers={
367
+ "Content-Type": "application/json",
368
+ "Accept": "application/json, text/event-stream",
369
+ "User-Agent": "platform-context-query/0.1",
370
+ },
371
+ method="POST",
372
+ )
373
+ try:
374
+ context = ssl.create_default_context()
375
+ with urllib.request.urlopen(request, timeout=READ_TIMEOUT_SECONDS, context=context) as response:
376
+ text = response.read().decode("utf-8", errors="replace")
377
+ answer = parse_sse_response(text)
378
+ if not answer:
379
+ return False, "[provider_error] empty response", True
380
+ if answer.startswith("[provider_error]"):
381
+ return False, answer, True
382
+ if "429 Too Many Requests" in answer or "Client error '429" in answer:
383
+ return False, answer, True
384
+ if provider_text_failure(answer):
385
+ return False, f"[provider_error] {answer}", True
386
+ return True, truncate_response(answer), False
387
+ except urllib.error.HTTPError as exc:
388
+ return False, f"[http_error] {exc.code} {exc.reason}", exc.code in {429, 500, 502, 503, 504}
389
+ except urllib.error.URLError as exc:
390
+ return False, f"[network_error] {exc.reason}", True
391
+ except TimeoutError:
392
+ return False, "[timeout]", True
393
+ except Exception as exc:
394
+ return False, f"[provider_exception] {type(exc).__name__}: {exc}", False
395
+
396
+
397
+ def ask_deepwiki(repo: str, question: str) -> tuple[bool, str]:
398
+ last_response = ""
399
+ for attempt in range(MAX_RETRIES + 1):
400
+ ok, response, retryable = ask_deepwiki_once(repo, question)
401
+ if ok:
402
+ return True, response
403
+ last_response = response
404
+ if not retryable or attempt >= MAX_RETRIES:
405
+ break
406
+ time.sleep(retry_wait_seconds(attempt))
407
+ return False, last_response
408
+
409
+
410
+ def query_one(query: dict[str, str]) -> tuple[bool, dict[str, Any]]:
411
+ query_type = query.get("type", "pattern")
412
+ ok, response = ask_deepwiki(DEEPWIKI_REPO, query.get("query", ""))
413
+ entry = {
414
+ "ts": dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"),
415
+ "provider": "deepwiki",
416
+ "repo": DEEPWIKI_REPO,
417
+ "module": query.get("module", ""),
418
+ "type": query_type,
419
+ "query": query.get("query", ""),
420
+ "response": response,
421
+ "quality": "verified" if ok else "query_failed",
422
+ }
423
+ return ok, entry
424
+
425
+
426
+ def run_deepwiki_provider(queries: list[dict[str, str]], cache_path: Path) -> tuple[str, str]:
427
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
428
+ stderr_lines: list[str] = []
429
+ ok_count = 0
430
+ entries: list[dict[str, Any]] = []
431
+ with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(queries) or 1)) as pool:
432
+ futures = {pool.submit(query_one, query): query for query in queries}
433
+ for future in as_completed(futures):
434
+ query = futures[future]
435
+ ok, entry = future.result()
436
+ entries.append(entry)
437
+ if ok:
438
+ ok_count += 1
439
+ else:
440
+ stderr_lines.append(f"{query.get('module', '')}/{query.get('type', '')}: {entry.get('response', '')}")
441
+ entries.sort(key=lambda item: str(item.get("type", "")))
442
+ with cache_path.open("a", encoding="utf-8") as handle:
443
+ for entry in entries:
444
+ handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
445
+ stdout = f"Queried: {ok_count}, Failed: {len(queries) - ok_count}, Total: {len(queries)}"
446
+ return stdout, "\n".join(stderr_lines)
447
+
448
+
449
+ def parse_args() -> argparse.Namespace:
450
+ parser = argparse.ArgumentParser(description="Query platform context evidence.")
451
+ parser.add_argument("--request", required=True, help="PlatformContextRequest JSON.")
452
+ parser.add_argument("--out-dir", required=True, help="Output directory.")
453
+ parser.add_argument("--api-level", type=int, help="Override API level. Defaults to request.api_level or 22.")
454
+ parser.add_argument("--dry-run", action="store_true", help="Build files without calling provider.")
455
+ return parser.parse_args()
456
+
457
+
458
+ def main() -> int:
459
+ args = parse_args()
460
+ started = time.time()
461
+
462
+ request_path = Path(args.request)
463
+ out_dir = Path(args.out_dir)
464
+ provider_dir = out_dir / "provider"
465
+ result_path = out_dir / "platform-context-result.json"
466
+ trace_path = out_dir / "platform-context-trace.json"
467
+ queries_path = provider_dir / "provider-queries.json"
468
+ cache_path = provider_dir / "provider-cache.jsonl"
469
+
470
+ raw_request = load_json(request_path)
471
+ api_level, api_level_source = resolve_api_level(raw_request, args.api_level)
472
+ request = validate_request(raw_request, api_level)
473
+ queries = build_queries(request)
474
+
475
+ out_dir.mkdir(parents=True, exist_ok=True)
476
+ provider_dir.mkdir(parents=True, exist_ok=True)
477
+ cache_path.unlink(missing_ok=True)
478
+ shutil.copyfile(request_path, out_dir / "platform-context-request.json")
479
+ write_json(queries_path, queries)
480
+
481
+ status = "dry_run" if args.dry_run else "ok"
482
+ provider_stdout = ""
483
+ provider_stderr = ""
484
+ exit_code = 0
485
+
486
+ if not args.dry_run:
487
+ provider_stdout, provider_stderr = run_deepwiki_provider(queries, cache_path)
488
+
489
+ evidence = read_cache_entries(cache_path)
490
+ verified_count = sum(1 for item in evidence if item.get("quality") == "verified")
491
+ failed_count = len(evidence) - verified_count
492
+ structured_missing_count = 0
493
+ if not args.dry_run:
494
+ if verified_count == 0:
495
+ status = "provider_failed"
496
+ exit_code = 1
497
+ elif failed_count:
498
+ status = "partial_ok"
499
+ elapsed_ms = int((time.time() - started) * 1000)
500
+
501
+ evidence_list = []
502
+ structured_evidence_list = []
503
+ for item in evidence:
504
+ raw_response = item.get("response", "")
505
+ cleaned, structured = clean_evidence_response(str(raw_response))
506
+ if item.get("quality") == "verified" and not structured:
507
+ structured_missing_count += 1
508
+ evidence_list.append({
509
+ "type": item.get("type", ""),
510
+ "quality": item.get("quality", ""),
511
+ "query": item.get("query", ""),
512
+ "response": raw_response,
513
+ })
514
+ structured_evidence_list.append({
515
+ "type": item.get("type", ""),
516
+ "quality": item.get("quality", ""),
517
+ "structured": structured,
518
+ "response": cleaned,
519
+ })
520
+
521
+ result = {
522
+ "status": status,
523
+ "stage": request["stage"],
524
+ "api_level": api_level,
525
+ "api_level_source": api_level_source,
526
+ "focus_point": request["focus_point"],
527
+ "platform_surfaces": request["platform_surfaces"],
528
+ "evidence_count": len(evidence),
529
+ "verified_evidence_count": verified_count,
530
+ "failed_evidence_count": failed_count,
531
+ "evidence": evidence_list,
532
+ "structured_evidence": structured_evidence_list,
533
+ "structured_missing_count": structured_missing_count,
534
+ "llm_usage": [
535
+ "Use evidence only if it changes or confirms the platform contract, local API usage, forbidden path, fallback, blocking unknown, or validation condition.",
536
+ "If API facts and pattern facts disagree, do not merge them silently. Prefer the stricter lifecycle / permission / forbidden-path constraint; if the implementation path still depends on the disagreement, record blocking Unknown or platform_drift.",
537
+ "Do not paste raw evidence into plan.md or commit-info.md.",
538
+ "If status is not ok and the platform fact blocks the task, record blocking Unknown or platform_drift.",
539
+ ],
540
+ }
541
+ trace = {
542
+ "status": status,
543
+ "provider": "deepwiki",
544
+ "api_level": api_level,
545
+ "api_level_source": api_level_source,
546
+ "query_count": len(queries),
547
+ "evidence_count": len(evidence),
548
+ "verified_evidence_count": verified_count,
549
+ "failed_evidence_count": failed_count,
550
+ "structured_missing_count": structured_missing_count,
551
+ "elapsed_ms": elapsed_ms,
552
+ "dry_run": bool(args.dry_run),
553
+ "provider_stdout": provider_stdout,
554
+ "provider_stderr": provider_stderr,
555
+ "internal_files": {
556
+ "queries": str(queries_path),
557
+ "cache": str(cache_path),
558
+ },
559
+ }
560
+
561
+ write_json(result_path, result)
562
+ write_json(trace_path, trace)
563
+ print(result_path)
564
+ return exit_code
565
+
566
+
567
+ if __name__ == "__main__":
568
+ raise SystemExit(main())