claudecode-omc 5.6.7 → 5.6.8

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.
Files changed (123) hide show
  1. package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
  2. package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
  3. package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
  4. package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
  5. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  6. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  7. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  8. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  9. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  10. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  11. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  12. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  13. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  14. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  15. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  16. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  17. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  18. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  19. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  20. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  21. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  22. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  23. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  24. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  25. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  26. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  27. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  28. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  29. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  30. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  31. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  32. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  33. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  34. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  35. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  36. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  37. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  38. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  39. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  40. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  41. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  42. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  43. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  44. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  45. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  46. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  47. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  48. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  49. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  50. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  51. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  52. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  53. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  54. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  55. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  56. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  57. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  58. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  59. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  60. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  61. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  62. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  63. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  64. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  65. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  66. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  67. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  68. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  69. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  70. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  71. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  72. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  73. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  74. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  75. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  76. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  77. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  78. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  79. package/.local/skills/swiftui-pro/references/api.md +39 -0
  80. package/.local/skills/swiftui-pro/references/data.md +43 -0
  81. package/.local/skills/swiftui-pro/references/design.md +32 -0
  82. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  83. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  84. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  85. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  86. package/.local/skills/swiftui-pro/references/views.md +36 -0
  87. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  88. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  89. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  90. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  91. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  92. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  93. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  94. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  95. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  96. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  97. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  98. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  99. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  100. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  101. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  102. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  103. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  104. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  105. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  106. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  107. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  108. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  109. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  110. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  111. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  112. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  113. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  114. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  115. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  116. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  117. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  118. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  119. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  120. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  121. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  122. package/bundled/manifest.json +1 -1
  123. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python3
2
+ """Analyze an Xcode Instruments .trace file and emit JSON + markdown.
3
+
4
+ Primary modes:
5
+ (default) Full four-lane analysis + cross-lane correlations.
6
+ --list-logs Dump os_log entries (optionally filtered) as JSON so an
7
+ agent can locate a focus window by log content.
8
+ --list-signposts Dump os_signpost intervals + point events as JSON.
9
+
10
+ Windowing:
11
+ --window START_MS:END_MS restricts every lane to that slice of the trace.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from instruments_parser import (
21
+ causes,
22
+ correlate,
23
+ events,
24
+ hangs,
25
+ hitches,
26
+ summary,
27
+ swiftui,
28
+ time_profiler,
29
+ xctrace,
30
+ )
31
+
32
+
33
+ def main(argv: list[str] | None = None) -> int:
34
+ parser = argparse.ArgumentParser(
35
+ description="Analyze an Instruments .trace file.",
36
+ )
37
+ parser.add_argument("--trace", required=True, type=Path)
38
+ parser.add_argument(
39
+ "--output",
40
+ type=Path,
41
+ help="Base path; writes <output>.json and <output>.md",
42
+ )
43
+ parser.add_argument("--top", type=int, default=10, help="Top-N per lane")
44
+ parser.add_argument(
45
+ "--top-hitches",
46
+ type=int,
47
+ default=5,
48
+ help="Correlate only the N worst hitches (avoid flooding output).",
49
+ )
50
+ parser.add_argument(
51
+ "--window",
52
+ type=str,
53
+ default=None,
54
+ help="Restrict analysis to a time slice, e.g. --window 10400:11700 (ms).",
55
+ )
56
+ parser.add_argument(
57
+ "--run",
58
+ type=int,
59
+ default=None,
60
+ help="Which run to analyze (1-based). Required for traces with >1 run.",
61
+ )
62
+ parser.add_argument(
63
+ "--list-runs", action="store_true",
64
+ help="Emit per-run metadata as JSON (use this to discover available runs).",
65
+ )
66
+
67
+ # Mode flags (mutually exclusive with full analysis)
68
+ mode_group = parser.add_argument_group("Discovery modes")
69
+ mode_group.add_argument(
70
+ "--list-logs", action="store_true",
71
+ help="Emit os_log entries as JSON (use filter flags below).",
72
+ )
73
+ mode_group.add_argument(
74
+ "--list-signposts", action="store_true",
75
+ help="Emit os_signpost intervals + events as JSON.",
76
+ )
77
+ mode_group.add_argument("--log-subsystem", type=str, default=None)
78
+ mode_group.add_argument("--log-category", type=str, default=None)
79
+ mode_group.add_argument(
80
+ "--log-type", type=str, default=None,
81
+ help="e.g. Fault, Error, Default, Info, Debug",
82
+ )
83
+ mode_group.add_argument(
84
+ "--log-message-contains", type=str, default=None,
85
+ help="Case-insensitive substring match on the message / format string.",
86
+ )
87
+ mode_group.add_argument(
88
+ "--log-limit", type=int, default=None,
89
+ help="Cap number of log entries returned (applied after all filters).",
90
+ )
91
+ mode_group.add_argument(
92
+ "--signpost-name-contains", type=str, default=None,
93
+ help="Case-insensitive substring match on signpost name.",
94
+ )
95
+ mode_group.add_argument("--signpost-subsystem", type=str, default=None)
96
+ mode_group.add_argument("--signpost-category", type=str, default=None)
97
+ mode_group.add_argument(
98
+ "--fanin-for", type=str, default=None,
99
+ help="Emit incoming cause-graph sources for destinations whose fmt "
100
+ "contains this substring. Case-insensitive.",
101
+ )
102
+
103
+ fmt_group = parser.add_mutually_exclusive_group()
104
+ fmt_group.add_argument("--json-only", action="store_true")
105
+ fmt_group.add_argument("--markdown-only", action="store_true")
106
+
107
+ args = parser.parse_args(argv)
108
+
109
+ # The discovery modes aren't in a mutually_exclusive_group because they
110
+ # live alongside their sub-filters in the same argparse group; enforce the
111
+ # constraint by hand so an agent gets a clear error instead of silent
112
+ # precedence.
113
+ active_modes = sum([
114
+ args.list_runs,
115
+ args.list_logs,
116
+ args.list_signposts,
117
+ bool(args.fanin_for),
118
+ ])
119
+ if active_modes > 1:
120
+ parser.error(
121
+ "--list-runs, --list-logs, --list-signposts, and --fanin-for are "
122
+ "mutually exclusive; pick one per invocation."
123
+ )
124
+
125
+ trace = args.trace
126
+ if not trace.exists():
127
+ print(f"error: trace not found: {trace}", file=sys.stderr)
128
+ return 2
129
+
130
+ info = xctrace.toc(trace)
131
+ window_ns = _parse_window(args.window)
132
+
133
+ if args.list_runs:
134
+ sys.stdout.write(json.dumps({
135
+ "xctrace_version": info.xctrace_version,
136
+ "runs": [
137
+ {
138
+ "number": r.number,
139
+ "template": r.template_name,
140
+ "duration_s": r.duration_s,
141
+ "start_date": r.start_date,
142
+ "end_date": r.end_date,
143
+ "schemas": sorted(r.schemas),
144
+ }
145
+ for r in info.runs
146
+ ],
147
+ }, indent=2))
148
+ sys.stdout.write("\n")
149
+ return 0
150
+
151
+ run_info = _resolve_run(info, args.run)
152
+ if run_info is None:
153
+ return 2
154
+ run_number = run_info.number
155
+
156
+ if args.list_logs:
157
+ out = events.list_logs(
158
+ trace, run_info.schemas,
159
+ subsystem=args.log_subsystem,
160
+ category=args.log_category,
161
+ message_contains=args.log_message_contains,
162
+ message_type=args.log_type,
163
+ limit=args.log_limit,
164
+ window_ns=window_ns,
165
+ run=run_number,
166
+ )
167
+ sys.stdout.write(json.dumps({"logs": out, "count": len(out)}, indent=2))
168
+ sys.stdout.write("\n")
169
+ return 0
170
+
171
+ if args.list_signposts:
172
+ sp = events.list_signposts(
173
+ trace, run_info.schemas,
174
+ name_contains=args.signpost_name_contains,
175
+ subsystem=args.signpost_subsystem,
176
+ category=args.signpost_category,
177
+ window_ns=window_ns,
178
+ run=run_number,
179
+ )
180
+ sys.stdout.write(json.dumps(sp, indent=2))
181
+ sys.stdout.write("\n")
182
+ return 0
183
+
184
+ if args.fanin_for:
185
+ fanin = causes.fanin_for(
186
+ trace, run_info.schemas,
187
+ destination_contains=args.fanin_for,
188
+ top_k=args.top,
189
+ window=window_ns,
190
+ run=run_number,
191
+ )
192
+ sys.stdout.write(json.dumps(fanin, indent=2))
193
+ sys.stdout.write("\n")
194
+ return 0
195
+
196
+ # Full five-lane analysis
197
+ schemas = run_info.schemas
198
+ lanes_out = {
199
+ "time-profiler": time_profiler.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
200
+ "hangs": hangs.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
201
+ "hitches": hitches.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
202
+ "swiftui": swiftui.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
203
+ "swiftui-causes": causes.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
204
+ }
205
+ correlations = correlate.build(
206
+ lanes_out, top_hitches=args.top_hitches, top_symbols=5
207
+ )
208
+ public_lanes = [_strip_internal(l) for l in lanes_out.values()]
209
+
210
+ result: dict = {
211
+ "trace": str(trace),
212
+ "xctrace_version": info.xctrace_version,
213
+ "run": run_number,
214
+ "runs_available": [r.number for r in info.runs],
215
+ "template": run_info.template_name,
216
+ "duration_s": run_info.duration_s,
217
+ "start_date": run_info.start_date,
218
+ "end_date": run_info.end_date,
219
+ "schemas_available": sorted(run_info.schemas),
220
+ "lanes": public_lanes,
221
+ "correlations": correlations,
222
+ }
223
+ if window_ns is not None:
224
+ result["window_ms"] = {
225
+ "start": window_ns[0] / 1_000_000,
226
+ "end": window_ns[1] / 1_000_000,
227
+ }
228
+
229
+ md = summary.render(result)
230
+
231
+ if args.output:
232
+ json_path = args.output.with_suffix(".json")
233
+ md_path = args.output.with_suffix(".md")
234
+ json_path.write_text(json.dumps(result, indent=2))
235
+ md_path.write_text(md)
236
+ print(f"wrote {json_path}")
237
+ print(f"wrote {md_path}")
238
+ return 0
239
+
240
+ if args.markdown_only:
241
+ sys.stdout.write(md)
242
+ elif args.json_only:
243
+ sys.stdout.write(json.dumps(result, indent=2))
244
+ sys.stdout.write("\n")
245
+ else:
246
+ sys.stdout.write(json.dumps(result, indent=2))
247
+ sys.stdout.write("\n---\n")
248
+ sys.stdout.write(md)
249
+ return 0
250
+
251
+
252
+ def _resolve_run(info, requested: int | None):
253
+ """Pick a run from the trace.
254
+
255
+ If `requested` is given, return that run or None on miss (with a friendly
256
+ error). If unset and the trace has exactly one run, default to it. If
257
+ unset and there are multiple runs, error out so the agent picks
258
+ explicitly — silently picking run 1 lost data for the user.
259
+ """
260
+ if not info.runs:
261
+ print("error: trace has no runs", file=sys.stderr)
262
+ return None
263
+ if requested is not None:
264
+ try:
265
+ return info.get_run(requested)
266
+ except KeyError as e:
267
+ print(f"error: {e}", file=sys.stderr)
268
+ return None
269
+ if len(info.runs) == 1:
270
+ return info.runs[0]
271
+ available = ", ".join(str(r.number) for r in info.runs)
272
+ print(
273
+ f"error: trace has {len(info.runs)} runs ({available}); pass --run N. "
274
+ f"Use --list-runs to see per-run metadata.",
275
+ file=sys.stderr,
276
+ )
277
+ return None
278
+
279
+
280
+ def _parse_window(spec: str | None) -> tuple[int, int] | None:
281
+ if not spec:
282
+ return None
283
+ if ":" not in spec:
284
+ raise SystemExit(f"--window expects START_MS:END_MS, got {spec!r}")
285
+ start_s, end_s = spec.split(":", 1)
286
+ try:
287
+ start_ms = float(start_s)
288
+ end_ms = float(end_s)
289
+ except ValueError as e:
290
+ raise SystemExit(f"--window: {e}")
291
+ if end_ms < start_ms:
292
+ raise SystemExit("--window: end_ms must be >= start_ms")
293
+ return (int(start_ms * 1_000_000), int(end_ms * 1_000_000))
294
+
295
+
296
+ def _strip_internal(lane: dict) -> dict:
297
+ return {k: v for k, v in lane.items() if not k.startswith("_")}
298
+
299
+
300
+ if __name__ == "__main__":
301
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Parsers for Xcode Instruments .trace files via xctrace export."""
@@ -0,0 +1,187 @@
1
+ """SwiftUI cause-graph lane (`swiftui-causes` schema).
2
+
3
+ Instruments emits one row per edge in SwiftUI's dependency graph: every time
4
+ a source node (a state change, user defaults observer, system event, etc.)
5
+ propagates to a destination node (a body evaluation, layout, creation), a
6
+ row is written with both endpoints as metadata values.
7
+
8
+ This lane aggregates those edges two ways:
9
+
10
+ - **By source node** — which attribute graph nodes are driving the most
11
+ updates overall. The canonical "why is my app thrashing?" view; a
12
+ `UserDefaultObserver.send()` showing up with 11k outgoing edges is a
13
+ feedback storm.
14
+ - **By destination node** — which views/modifiers receive the most
15
+ invalidations, and from whom. Use this to trace a hot view back to the
16
+ source that keeps poking it.
17
+
18
+ The analyzer's main lane (`swiftui`) tells you *what* updates are
19
+ expensive; this lane tells you *why* they keep happening.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from collections import Counter, defaultdict
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from . import xctrace, xml_utils
28
+
29
+ SCHEMA = "swiftui-causes"
30
+
31
+ # Metadata nodes render as space-separated field dumps ("A gray icon n/a n/a").
32
+ # We aggregate on the full fmt string so callers can spot specific edges like
33
+ # "@AppStorage TextStyleModifier.fontOption", but also expose the short head
34
+ # ("@AppStorage", "Creation of App", ...) for coarser grouping.
35
+
36
+
37
+ def analyze(
38
+ trace_path: Path,
39
+ toc_schemas: frozenset[str],
40
+ top_n: int = 10,
41
+ top_k_per_node: int = 5,
42
+ window: tuple[int, int] | None = None,
43
+ run: int = 1,
44
+ ) -> dict[str, Any]:
45
+ if SCHEMA not in toc_schemas:
46
+ return {
47
+ "lane": "swiftui-causes",
48
+ "available": False,
49
+ "notes": [
50
+ "SwiftUI causes data not present (requires SwiftUI template on a real device).",
51
+ ],
52
+ }
53
+
54
+ xml_bytes = xctrace.export_schema(trace_path, SCHEMA, run=run)
55
+ stream = xml_utils.RowStream(xml_bytes)
56
+
57
+ source_edges: Counter[str] = Counter()
58
+ destination_edges: Counter[str] = Counter()
59
+ fanout: dict[str, Counter[str]] = defaultdict(Counter)
60
+ fanin: dict[str, Counter[str]] = defaultdict(Counter)
61
+ label_counts: Counter[str] = Counter()
62
+ total_edges = 0
63
+
64
+ for row in stream:
65
+ time_el = xml_utils.first_present(row, "timestamp", "time")
66
+ if time_el is not None:
67
+ t_ns = xml_utils.int_text(stream.resolve(time_el))
68
+ if t_ns is not None and not xml_utils.in_window(t_ns, window):
69
+ continue
70
+
71
+ src = _fmt(row, stream, "source-node")
72
+ dst = _fmt(row, stream, "destination-node")
73
+ if not src or not dst:
74
+ continue
75
+
76
+ source_edges[src] += 1
77
+ destination_edges[dst] += 1
78
+ fanout[src][dst] += 1
79
+ fanin[dst][src] += 1
80
+
81
+ label = _fmt(row, stream, "label")
82
+ if label:
83
+ label_counts[label] += 1
84
+
85
+ total_edges += 1
86
+
87
+ top_sources = [
88
+ {
89
+ "source": src,
90
+ "edges": count,
91
+ "top_destinations": [
92
+ {"destination": d, "edges": c}
93
+ for d, c in fanout[src].most_common(top_k_per_node)
94
+ ],
95
+ }
96
+ for src, count in source_edges.most_common(top_n)
97
+ ]
98
+
99
+ top_destinations = [
100
+ {
101
+ "destination": dst,
102
+ "edges": count,
103
+ "top_sources": [
104
+ {"source": s, "edges": c}
105
+ for s, c in fanin[dst].most_common(top_k_per_node)
106
+ ],
107
+ }
108
+ for dst, count in destination_edges.most_common(top_n)
109
+ ]
110
+
111
+ return {
112
+ "lane": "swiftui-causes",
113
+ "available": True,
114
+ "schema_used": SCHEMA,
115
+ "metrics": {
116
+ "total_edges": total_edges,
117
+ "unique_sources": len(source_edges),
118
+ "unique_destinations": len(destination_edges),
119
+ "top_labels": dict(label_counts.most_common(top_n)),
120
+ },
121
+ "top_sources": top_sources,
122
+ "top_destinations": top_destinations,
123
+ "notes": [],
124
+ }
125
+
126
+
127
+ def fanin_for(
128
+ trace_path: Path,
129
+ toc_schemas: frozenset[str],
130
+ destination_contains: str,
131
+ top_k: int = 10,
132
+ window: tuple[int, int] | None = None,
133
+ run: int = 1,
134
+ ) -> dict[str, Any]:
135
+ """Return the top source nodes feeding any destination whose fmt string
136
+ contains `destination_contains` (case-insensitive substring).
137
+
138
+ Used when the agent has a suspect view from the `swiftui` lane and wants
139
+ to know *who keeps invalidating it*. Does a full pass over the causes
140
+ schema each time — cheap enough at typical trace sizes.
141
+ """
142
+ if SCHEMA not in toc_schemas:
143
+ return {"available": False, "matches": []}
144
+
145
+ needle = destination_contains.lower()
146
+ xml_bytes = xctrace.export_schema(trace_path, SCHEMA, run=run)
147
+ stream = xml_utils.RowStream(xml_bytes)
148
+
149
+ matches: dict[str, Counter[str]] = defaultdict(Counter)
150
+ totals: Counter[str] = Counter()
151
+
152
+ for row in stream:
153
+ time_el = xml_utils.first_present(row, "timestamp", "time")
154
+ if time_el is not None:
155
+ t_ns = xml_utils.int_text(stream.resolve(time_el))
156
+ if t_ns is not None and not xml_utils.in_window(t_ns, window):
157
+ continue
158
+
159
+ dst = _fmt(row, stream, "destination-node")
160
+ if not dst or needle not in dst.lower():
161
+ continue
162
+ src = _fmt(row, stream, "source-node")
163
+ if not src:
164
+ continue
165
+
166
+ matches[dst][src] += 1
167
+ totals[dst] += 1
168
+
169
+ out = []
170
+ for dst, count in totals.most_common(top_k):
171
+ out.append({
172
+ "destination": dst,
173
+ "total_incoming_edges": count,
174
+ "top_sources": [
175
+ {"source": s, "edges": c}
176
+ for s, c in matches[dst].most_common(top_k)
177
+ ],
178
+ })
179
+ return {"available": True, "matches": out}
180
+
181
+
182
+ def _fmt(row, stream, key: str) -> str | None:
183
+ el = row.get(key)
184
+ if el is None:
185
+ return None
186
+ resolved = stream.resolve(el)
187
+ return resolved.get("fmt") or xml_utils.str_text(resolved)