its-magic 0.1.3-2 → 0.1.3-4

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,543 @@
1
+ """
2
+ Work-kind classification + tiered delivery routing per story (US-0118 / DEC-0118).
3
+
4
+ Deterministic pure-stdlib per-story work-kind classifier that returns
5
+ ``work_kind ∈ {doc, mini, code}`` plus a recommended delivery mode and phase
6
+ plan. Reuses ``dev_environment_lib.classify_touched_files`` (tier A/B/C +
7
+ ``TIER_C_SKIP_PREFIXES``) via import — never duplicates the tier rules.
8
+
9
+ The classifier is gated by the ``WORK_KIND_ROUTING`` scratchpad flag
10
+ (default ``0`` — zero overhead when off). When ``1`` the classifier is
11
+ consulted by ``/intake`` step 5 and ``/auto`` ``resolve_delivery_mode``
12
+ step 0 per the L8 precedence chain (explicit ``DELIVERY_MODE`` >
13
+ ``AUTO_PHASE_*`` > ``WORK_KIND_ROUTING``-derived > current default).
14
+
15
+ Reason codes (``WORK_KIND_*`` family, R-0106 Q2 LOCKED):
16
+ - ``WORK_KIND_ROUTING_OFF`` — info-only; ``WORK_KIND_ROUTING != "1"``.
17
+ - ``WORK_KIND_DELIVERY_MODE_CONFLICT`` — explicit ``DELIVERY_MODE`` set
18
+ and conflicts with the classifier recommendation.
19
+ - ``WORK_KIND_CLASSIFY_FAILED`` — classifier raised / returned malformed.
20
+ - ``WORK_KIND_UNKNOWN_ROUTE`` — work_kind value not in {doc, mini, code}.
21
+ - ``WORK_KIND_PLAN_COVERAGE_MISSING`` — empty/invalid phase plan.
22
+ - ``WORK_KIND_TIE_BREAK_APPLIED`` — info-only; mixed tiers resolved by
23
+ the highest-tier-wins tie-break (Q1 LOCKED).
24
+
25
+ Intake evidence schema extension (AC-9 / R-0106 Q9): the intake evidence
26
+ JSON gains three optional fields when the classifier runs at ``/intake``
27
+ step 5 — ``work_kind``, ``recommended_delivery_mode``,
28
+ ``work_kind_operator_decision ∈ {accept, override}``. Existing intake
29
+ evidence files are NOT modified; only the schema contract is documented
30
+ here.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ import os
37
+ import sys
38
+ from dataclasses import asdict, dataclass, field
39
+ from enum import Enum
40
+ from typing import Any, Dict, List, Optional, Tuple
41
+
42
+ # Path bootstrap so the bare ``import dev_environment_lib`` resolves
43
+ # whether the script is run from the repo root or as a file path.
44
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
45
+ if _SCRIPT_DIR not in sys.path:
46
+ sys.path.insert(0, _SCRIPT_DIR)
47
+
48
+ # Q9 LOCKED import contract — import, do NOT duplicate. The tier A/B/C
49
+ # classification + skip-prefix single source of truth lives in
50
+ # ``dev_environment_lib`` so that drift between the two rule engines is
51
+ # impossible by construction.
52
+ import dev_environment_lib # noqa: E402
53
+ from dev_environment_lib import ( # noqa: E402
54
+ TIER_C_SKIP_PREFIXES,
55
+ classify_touched_files,
56
+ )
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Enumeration + dataclass (R-0106 Q10 LOCKED signature)
61
+ # ---------------------------------------------------------------------------
62
+
63
+
64
+ class WorkKind(str, Enum):
65
+ """Three-tier work-kind enumeration (DEC-0118 §1)."""
66
+
67
+ DOC = "doc"
68
+ MINI = "mini"
69
+ CODE = "code"
70
+
71
+
72
+ @dataclass
73
+ class WorkKindClassification:
74
+ """
75
+ Result of :func:`classify_work_kind`.
76
+
77
+ Fields mirror R-0106 Q10 LOCKED signature. ``rule_trace`` is populated
78
+ only when ``explain=True`` (Q3 LOCKED — ``--explain`` flag).
79
+ """
80
+
81
+ work_kind: WorkKind
82
+ recommended_delivery_mode: str
83
+ recommended_phase_plan: List[str]
84
+ rationale: str
85
+ evidence_refs: List[str] = field(default_factory=list)
86
+ rule_trace: List[Tuple[str, str, str]] = field(default_factory=list)
87
+ # Intake-evidence schema extension fields (AC-9). Populated so that
88
+ # consumers (e.g. ``/intake`` step 5) can persist them directly into
89
+ # the intake evidence bundle without re-deriving.
90
+ work_kind_operator_decision: Optional[str] = None
91
+
92
+ def as_dict(self) -> Dict[str, Any]:
93
+ """Serialize to a JSON-friendly dict (enum → string)."""
94
+ d = asdict(self)
95
+ d["work_kind"] = self.work_kind.value
96
+ d["rule_trace"] = [list(t) for t in self.rule_trace]
97
+ return d
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Constants — phase plans + reason codes (R-0106 Q2 LOCKED)
102
+ # ---------------------------------------------------------------------------
103
+
104
+
105
+ DOC_PHASE_PLAN: Tuple[str, ...] = ("intake", "execute", "release")
106
+ MINI_ULTRA_LEAN_PHASE_PLAN: Tuple[str, ...] = (
107
+ "spec",
108
+ "plan",
109
+ "build+verify",
110
+ "ship",
111
+ )
112
+ MINI_MEGA_QUICK_PHASE_PLAN: Tuple[str, ...] = ("quick",)
113
+ CODE_STANDARD_PHASE_PLAN: Tuple[str, ...] = (
114
+ "intake",
115
+ "discovery",
116
+ "research",
117
+ "architecture",
118
+ "sprint-plan",
119
+ "plan-verify",
120
+ "execute",
121
+ "qa",
122
+ "verify-work",
123
+ "release",
124
+ "refresh-context",
125
+ )
126
+
127
+ # Reason-code family (R-0106 Q2 LOCKED + AC-7). ``WORK_KIND_ROUTING_OFF``
128
+ # and ``WORK_KIND_TIE_BREAK_APPLIED`` are info-only (not fail-closed); the
129
+ # other four are fail-closed and emit remediation guidance in
130
+ # ``sprints/Sxxxx/qa-findings.md`` / ``release-findings.md``.
131
+ WORK_KIND_ROUTING_OFF = "WORK_KIND_ROUTING_OFF"
132
+ WORK_KIND_DELIVERY_MODE_CONFLICT = "WORK_KIND_DELIVERY_MODE_CONFLICT"
133
+ WORK_KIND_CLASSIFY_FAILED = "WORK_KIND_CLASSIFY_FAILED"
134
+ WORK_KIND_UNKNOWN_ROUTE = "WORK_KIND_UNKNOWN_ROUTE"
135
+ WORK_KIND_PLAN_COVERAGE_MISSING = "WORK_KIND_PLAN_COVERAGE_MISSING"
136
+ WORK_KIND_TIE_BREAK_APPLIED = "WORK_KIND_TIE_BREAK_APPLIED"
137
+
138
+ WORK_KIND_REASON_CODES: Tuple[str, ...] = (
139
+ WORK_KIND_ROUTING_OFF,
140
+ WORK_KIND_DELIVERY_MODE_CONFLICT,
141
+ WORK_KIND_CLASSIFY_FAILED,
142
+ WORK_KIND_UNKNOWN_ROUTE,
143
+ WORK_KIND_PLAN_COVERAGE_MISSING,
144
+ WORK_KIND_TIE_BREAK_APPLIED,
145
+ )
146
+
147
+ REASON_CODE_REMEDIATION: Dict[str, str] = {
148
+ WORK_KIND_ROUTING_OFF: (
149
+ "Set WORK_KIND_ROUTING=1 to enable per-story routing; current "
150
+ "behavior unchanged."
151
+ ),
152
+ WORK_KIND_DELIVERY_MODE_CONFLICT: (
153
+ "Explicit DELIVERY_MODE wins per L8 precedence; either unset "
154
+ "DELIVERY_MODE to allow work-kind routing or update the backlog "
155
+ "row; mid-story switch forbidden (DELIVERY_MODE_SWITCH_MID_STORY)."
156
+ ),
157
+ WORK_KIND_CLASSIFY_FAILED: (
158
+ "Re-run /intake with explicit work_kind override; inspect "
159
+ "--explain trace; file bug if rule engine is at fault."
160
+ ),
161
+ WORK_KIND_UNKNOWN_ROUTE: (
162
+ "Re-run classifier; if persistent, set DELIVERY_MODE explicitly "
163
+ "or add AUTO_PHASE_* override; default to standard lifecycle."
164
+ ),
165
+ WORK_KIND_PLAN_COVERAGE_MISSING: (
166
+ "Re-run classifier; if persistent, set DELIVERY_MODE explicitly "
167
+ "or add AUTO_PHASE_* override; default to standard lifecycle."
168
+ ),
169
+ WORK_KIND_TIE_BREAK_APPLIED: (
170
+ "Mixed-tier story resolved by highest-tier-wins tie-break "
171
+ "(code > mini > doc). Inspect --explain trace to override."
172
+ ),
173
+ }
174
+
175
+ # Tier → work_kind mapping (Q1 LOCKED). Mirrors
176
+ # ``classify_touched_files`` tier_rank A>B>C where A=code, B=mini, C=doc.
177
+ TIER_TO_WORK_KIND: Dict[str, WorkKind] = {
178
+ "A": WorkKind.CODE,
179
+ "B": WorkKind.MINI,
180
+ "C": WorkKind.DOC,
181
+ }
182
+
183
+ # US-0096 mega_quick eligibility signals (L6 LOCKED). The classifier
184
+ # recommends ``mega_quick`` only when all of these hold; otherwise it
185
+ # falls back to ``ultra_lean`` for the ``mini`` route.
186
+ MEGA_QUICK_MAX_AC = 3
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Helpers
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ def _looks_like_markdown_under_skip_prefix(rel: str) -> bool:
195
+ """Return True for ``*.md`` / ``README*`` under a skip prefix (L5)."""
196
+ norm = rel.replace("\\", "/")
197
+ if not any(norm.startswith(p) for p in TIER_C_SKIP_PREFIXES):
198
+ return False
199
+ base = norm.rsplit("/", 1)[-1]
200
+ return base.lower().endswith(".md") or base.lower().startswith("readme")
201
+
202
+
203
+ def _resolve_work_kind_from_tier(
204
+ tier: Optional[str],
205
+ touched_file_hints: List[str],
206
+ explain: bool,
207
+ ) -> Tuple[WorkKind, List[Tuple[str, str, str]]]:
208
+ """
209
+ Map the highest matched tier to a work_kind (Q1 LOCKED tie-break).
210
+
211
+ ``classify_touched_files`` already returns the *highest* matching tier
212
+ (A>B>C). When the story touches both ``docs/`` and ``src/`` paths the
213
+ highest tier wins — ``code`` > ``mini`` > ``doc``.
214
+ """
215
+ trace: List[Tuple[str, str, str]] = []
216
+ if tier is None:
217
+ # No matched source path. If every touched file is a doc/markdown
218
+ # under a skip prefix → DOC; otherwise fall back to DOC as the
219
+ # conservative default (no runtime surface detected).
220
+ all_doc = bool(touched_file_hints) and all(
221
+ _looks_like_markdown_under_skip_prefix(h) or any(
222
+ h.replace("\\", "/").startswith(p) for p in TIER_C_SKIP_PREFIXES
223
+ )
224
+ for h in touched_file_hints
225
+ )
226
+ if explain:
227
+ trace.append((
228
+ "rule.doc.no_runtime_surface",
229
+ f"tier={tier}, hints={len(touched_file_hints)}",
230
+ "doc" if all_doc else "doc (conservative default)",
231
+ ))
232
+ return WorkKind.DOC, trace
233
+
234
+ work_kind = TIER_TO_WORK_KIND.get(tier, WorkKind.DOC)
235
+ if explain:
236
+ trace.append((
237
+ "rule.tier.highest_wins",
238
+ f"tier={tier}",
239
+ work_kind.value,
240
+ ))
241
+ # Tie-break signal: when any touched file is under a skip prefix
242
+ # AND the highest tier is A or B, the highest-tier-wins rule
243
+ # resolved a mixed-tier story (Q1 LOCKED).
244
+ has_skip_prefix = any(
245
+ any(h.replace("\\", "/").startswith(p) for p in TIER_C_SKIP_PREFIXES)
246
+ for h in touched_file_hints
247
+ )
248
+ if has_skip_prefix and tier in ("A", "B"):
249
+ trace.append((
250
+ "rule.tie_break.highest_tier_wins",
251
+ f"tier={tier} + skip-prefix files present",
252
+ work_kind.value,
253
+ ))
254
+ return work_kind, trace
255
+
256
+
257
+ def _resolve_mini_delivery_mode(
258
+ ac_set: List[str],
259
+ component_scope: Optional[str],
260
+ has_companion_dec: bool,
261
+ explain: bool,
262
+ ) -> Tuple[str, List[Tuple[str, str, str]]]:
263
+ """
264
+ Resolve ``mini`` → ``ultra_lean`` or ``mega_quick`` per US-0096
265
+ eligibility (L6 LOCKED).
266
+ """
267
+ trace: List[Tuple[str, str, str]] = []
268
+ ac_count = len(ac_set)
269
+ single_component = component_scope is not None and "," not in str(component_scope)
270
+ eligible_mega_quick = (
271
+ ac_count <= MEGA_QUICK_MAX_AC
272
+ and not has_companion_dec
273
+ and single_component
274
+ )
275
+ if eligible_mega_quick:
276
+ if explain:
277
+ trace.append((
278
+ "rule.mini.mega_quick_eligible",
279
+ f"ac_count={ac_count}, dec={has_companion_dec}, "
280
+ f"component_scope={component_scope!r}",
281
+ "mega_quick",
282
+ ))
283
+ return "mega_quick", trace
284
+ if explain:
285
+ trace.append((
286
+ "rule.mini.ultra_lean_fallback",
287
+ f"ac_count={ac_count}, dec={has_companion_dec}, "
288
+ f"single_component={single_component}",
289
+ "ultra_lean",
290
+ ))
291
+ return "ultra_lean", trace
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # Public API
296
+ # ---------------------------------------------------------------------------
297
+
298
+
299
+ def classify_work_kind(
300
+ story_prose: str,
301
+ acceptance_criteria: List[str],
302
+ touched_file_hints: List[str],
303
+ component_scope: Optional[str] = None,
304
+ *,
305
+ has_companion_dec: bool = False,
306
+ explain: bool = False,
307
+ ) -> WorkKindClassification:
308
+ """
309
+ Classify a story into ``{doc, mini, code}`` and derive a recommended
310
+ delivery mode + phase plan.
311
+
312
+ Inputs are prose + AC set + touched-file hints (names-only, no content
313
+ reads) + component_scope string. Pure stdlib, no network, no ``.env``
314
+ reads, no LLM calls (Q3 LOCKED).
315
+
316
+ Parameters mirror R-0106 Q10 LOCKED signature. ``has_companion_dec``
317
+ and ``explain`` are keyword-only extensions surfaced for the
318
+ ``--explain`` CLI trace + the ``mini`` → ``mega_quick`` eligibility
319
+ check.
320
+ """
321
+ if not isinstance(touched_file_hints, list):
322
+ raise TypeError("touched_file_hints must be a list[str]")
323
+
324
+ # Reuse the canonical tier classifier (Q9 LOCKED import contract).
325
+ tier = classify_touched_files(touched_file_hints)
326
+ work_kind, trace = _resolve_work_kind_from_tier(
327
+ tier, touched_file_hints, explain
328
+ )
329
+
330
+ # Derive delivery mode + phase plan (DEC-0118 §1 mapping table).
331
+ if work_kind is WorkKind.DOC:
332
+ recommended_delivery_mode = "ultra_lean"
333
+ recommended_phase_plan = list(DOC_PHASE_PLAN)
334
+ if explain:
335
+ trace.append((
336
+ "rule.doc.lean_plan",
337
+ f"work_kind={work_kind.value}",
338
+ "ultra_lean + [intake, execute, release]",
339
+ ))
340
+ elif work_kind is WorkKind.MINI:
341
+ recommended_delivery_mode, mini_trace = _resolve_mini_delivery_mode(
342
+ acceptance_criteria, component_scope, has_companion_dec, explain
343
+ )
344
+ trace.extend(mini_trace)
345
+ if recommended_delivery_mode == "mega_quick":
346
+ recommended_phase_plan = list(MINI_MEGA_QUICK_PHASE_PLAN)
347
+ else:
348
+ recommended_phase_plan = list(MINI_ULTRA_LEAN_PHASE_PLAN)
349
+ else: # WorkKind.CODE
350
+ recommended_delivery_mode = "standard"
351
+ recommended_phase_plan = list(CODE_STANDARD_PHASE_PLAN)
352
+ if explain:
353
+ trace.append((
354
+ "rule.code.standard",
355
+ f"work_kind={work_kind.value}",
356
+ "standard + full canonical lifecycle",
357
+ ))
358
+
359
+ rationale = (
360
+ f"work_kind={work_kind.value} derived from "
361
+ f"classify_touched_files tier={tier}; "
362
+ f"recommended_delivery_mode={recommended_delivery_mode}; "
363
+ f"phase_plan={recommended_phase_plan}"
364
+ )
365
+
366
+ return WorkKindClassification(
367
+ work_kind=work_kind,
368
+ recommended_delivery_mode=recommended_delivery_mode,
369
+ recommended_phase_plan=recommended_phase_plan,
370
+ rationale=rationale,
371
+ evidence_refs=[], # names-only; populated by caller if needed
372
+ rule_trace=trace,
373
+ )
374
+
375
+
376
+ # ---------------------------------------------------------------------------
377
+ # Self-test (AC-12) — exits 0 on success
378
+ # ---------------------------------------------------------------------------
379
+
380
+
381
+ def self_test() -> int:
382
+ """Built-in self-test (AC-12). Exits 0 on success, 1 on failure."""
383
+ failures: List[str] = []
384
+
385
+ # DOC route — only docs/ touched files → tier None → DOC → lean plan.
386
+ doc = classify_work_kind(
387
+ "Update README",
388
+ ["AC-1 README updated"],
389
+ ["docs/engineering/runbook.md", "docs/product/backlog.md"],
390
+ )
391
+ if doc.work_kind is not WorkKind.DOC:
392
+ failures.append(f"doc work_kind: expected DOC, got {doc.work_kind}")
393
+ if doc.recommended_phase_plan != list(DOC_PHASE_PLAN):
394
+ failures.append(
395
+ f"doc phase_plan: expected {DOC_PHASE_PLAN}, got {doc.recommended_phase_plan}"
396
+ )
397
+ if doc.recommended_delivery_mode != "ultra_lean":
398
+ failures.append(
399
+ f"doc delivery_mode: expected ultra_lean, got {doc.recommended_delivery_mode}"
400
+ )
401
+
402
+ # MINI route — single env.example (tier B) → MINI → ultra_lean fallback
403
+ # (AC count > MEGA_QUICK_MAX_AC).
404
+ mini = classify_work_kind(
405
+ "Tweak nginx config",
406
+ [f"AC-{i}" for i in range(1, 5)], # 4 ACs > 3 → ultra_lean
407
+ [".env.example"],
408
+ component_scope="web",
409
+ )
410
+ if mini.work_kind is not WorkKind.MINI:
411
+ failures.append(f"mini work_kind: expected MINI, got {mini.work_kind}")
412
+ if mini.recommended_delivery_mode != "ultra_lean":
413
+ failures.append(
414
+ f"mini delivery_mode: expected ultra_lean, got {mini.recommended_delivery_mode}"
415
+ )
416
+
417
+ # MINI → mega_quick when eligible (≤3 ACs, single component, no DEC).
418
+ mini_mq = classify_work_kind(
419
+ "Tiny fix",
420
+ ["AC-1", "AC-2"],
421
+ [".env.example"],
422
+ component_scope="web",
423
+ )
424
+ if mini_mq.recommended_delivery_mode != "mega_quick":
425
+ failures.append(
426
+ f"mini mega_quick: expected mega_quick, got {mini_mq.recommended_delivery_mode}"
427
+ )
428
+
429
+ # CODE route — package.json (tier A) → CODE → standard.
430
+ code = classify_work_kind(
431
+ "New feature",
432
+ ["AC-1", "AC-2", "AC-3", "AC-4"],
433
+ ["package.json", "src/index.ts"],
434
+ )
435
+ if code.work_kind is not WorkKind.CODE:
436
+ failures.append(f"code work_kind: expected CODE, got {code.work_kind}")
437
+ if code.recommended_delivery_mode != "standard":
438
+ failures.append(
439
+ f"code delivery_mode: expected standard, got {code.recommended_delivery_mode}"
440
+ )
441
+
442
+ # Tie-break — mixed docs/ + src/ → CODE (highest tier wins, Q1 LOCKED).
443
+ mixed = classify_work_kind(
444
+ "Feature + docs",
445
+ ["AC-1"],
446
+ ["docs/foo.md", "package.json"],
447
+ )
448
+ if mixed.work_kind is not WorkKind.CODE:
449
+ failures.append(
450
+ f"tie-break: expected CODE (highest tier wins), got {mixed.work_kind}"
451
+ )
452
+
453
+ # --explain emits rule_trace.
454
+ explained = classify_work_kind(
455
+ "Tiny fix",
456
+ ["AC-1"],
457
+ [".env.example"],
458
+ component_scope="web",
459
+ explain=True,
460
+ )
461
+ if not explained.rule_trace:
462
+ failures.append("explain=True: rule_trace must be non-empty")
463
+
464
+ # Import boundary — classify_touched_files must be imported, not copied.
465
+ from work_kind_classify_lib import classify_touched_files as imported
466
+ if imported is not classify_touched_files:
467
+ failures.append("import boundary: classify_touched_files not reused")
468
+
469
+ if failures:
470
+ for f in failures:
471
+ print(f"[WORK_KIND_CLASSIFY_SELF_TEST_FAIL] {f}", file=sys.stderr)
472
+ return 1
473
+ print("[WORK_KIND_CLASSIFY_SELF_TEST_OK]")
474
+ return 0
475
+
476
+
477
+ # ---------------------------------------------------------------------------
478
+ # CLI
479
+ # ---------------------------------------------------------------------------
480
+
481
+
482
+ def main(argv: Optional[List[str]] = None) -> int:
483
+ p = argparse.ArgumentParser(
484
+ description="Work-kind classifier (US-0118 / DEC-0118)."
485
+ )
486
+ p.add_argument(
487
+ "--self-test",
488
+ action="store_true",
489
+ help="Run built-in self-test (AC-12).",
490
+ )
491
+ p.add_argument(
492
+ "--explain",
493
+ action="store_true",
494
+ help="Emit rule_trace in the JSON output (Q3 LOCKED).",
495
+ )
496
+ p.add_argument(
497
+ "--story-prose",
498
+ default="",
499
+ help="Story prose (names-only, no content reads).",
500
+ )
501
+ p.add_argument(
502
+ "--ac",
503
+ nargs="*",
504
+ default=[],
505
+ help="Acceptance criteria list.",
506
+ )
507
+ p.add_argument(
508
+ "--touched",
509
+ nargs="*",
510
+ default=[],
511
+ help="Touched file hints (names-only).",
512
+ )
513
+ p.add_argument(
514
+ "--component-scope",
515
+ default=None,
516
+ help="Component scope string (single component when set without comma).",
517
+ )
518
+ p.add_argument(
519
+ "--has-companion-dec",
520
+ action="store_true",
521
+ help="Story has a companion DEC (disables mega_quick eligibility).",
522
+ )
523
+ args = p.parse_args(argv)
524
+
525
+ if args.self_test:
526
+ return self_test()
527
+
528
+ result = classify_work_kind(
529
+ story_prose=args.story_prose,
530
+ acceptance_criteria=args.ac,
531
+ touched_file_hints=args.touched,
532
+ component_scope=args.component_scope,
533
+ has_companion_dec=args.has_companion_dec,
534
+ explain=args.explain,
535
+ )
536
+ import json
537
+
538
+ print(json.dumps(result.as_dict(), indent=2))
539
+ return 0
540
+
541
+
542
+ if __name__ == "__main__":
543
+ sys.exit(main())