claudecode-omc 5.6.6 → 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 (179) 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/h5-to-swiftui/SKILL.md +201 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  10. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  11. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  12. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  13. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  40. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  41. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  42. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  43. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  44. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  45. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  46. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  47. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  48. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  49. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  50. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  51. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  52. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  53. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  54. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  55. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  56. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  57. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  58. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  59. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  60. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  61. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  62. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  63. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  64. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  65. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  66. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  67. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  68. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  69. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  70. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  71. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  72. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  73. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  74. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  75. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  76. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  77. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  78. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  79. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  80. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  81. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  82. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  83. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  84. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  85. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  86. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  87. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  88. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  89. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  90. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  91. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  92. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  93. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  94. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  95. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  96. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  98. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  99. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  100. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  101. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  102. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  103. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  104. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  105. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  106. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  107. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  108. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  109. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  110. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  111. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  112. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  113. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  114. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  115. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  116. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  117. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  118. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  119. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  120. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  121. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  122. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  123. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  124. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  125. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  126. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  127. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  128. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  129. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  130. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  131. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  132. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  133. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  134. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  135. package/.local/skills/swiftui-pro/references/api.md +39 -0
  136. package/.local/skills/swiftui-pro/references/data.md +43 -0
  137. package/.local/skills/swiftui-pro/references/design.md +32 -0
  138. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  139. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  140. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  141. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  142. package/.local/skills/swiftui-pro/references/views.md +36 -0
  143. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  144. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  145. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  146. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  147. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  148. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  149. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  150. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  151. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  152. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  153. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  154. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  155. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  156. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  157. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  158. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  159. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  160. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  161. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  162. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  163. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  164. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  165. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  166. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  167. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  168. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  169. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  170. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  171. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  172. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  173. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  174. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  175. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  176. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  177. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  178. package/bundled/manifest.json +1 -1
  179. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ """Cross-lane correlation: for each hang and top-N worst hitches, aggregate
2
+ Time Profiler samples and SwiftUI updates whose timestamps fall inside the
3
+ event window [start, start+duration]. Uses bisect so lookups stay O(log N)
4
+ per event.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from bisect import bisect_left, bisect_right
9
+ from collections import defaultdict
10
+ from typing import Any
11
+
12
+
13
+ def build(lanes: dict[str, dict], top_hitches: int = 5, top_symbols: int = 5) -> list[dict]:
14
+ """Produce a list of correlation entries.
15
+
16
+ `lanes` is a dict keyed by lane name (time-profiler, hangs, hitches,
17
+ swiftui) of their analyzer outputs.
18
+ """
19
+ tp = lanes.get("time-profiler")
20
+ hangs = lanes.get("hangs")
21
+ hitches = lanes.get("hitches")
22
+ swiftui = lanes.get("swiftui")
23
+
24
+ tp_index = _build_time_profile_index(tp)
25
+ sui_events = (swiftui or {}).get("_events") if swiftui and swiftui.get("available") else None
26
+
27
+ correlations: list[dict] = []
28
+
29
+ if hangs and hangs.get("available"):
30
+ for h in hangs.get("_events", []):
31
+ correlations.append(
32
+ _correlate_event(
33
+ trigger_lane="hangs",
34
+ start_ns=h["start_ns"],
35
+ end_ns=h["end_ns"],
36
+ extra={"hang_type": h["hang_type"]},
37
+ tp_index=tp_index,
38
+ sui_events=sui_events,
39
+ top_symbols=top_symbols,
40
+ )
41
+ )
42
+
43
+ if hitches and hitches.get("available"):
44
+ worst_hitches = hitches.get("_events", [])[:top_hitches]
45
+ for hi in worst_hitches:
46
+ correlations.append(
47
+ _correlate_event(
48
+ trigger_lane="hitches",
49
+ start_ns=hi["start_ns"],
50
+ end_ns=hi["end_ns"],
51
+ extra={
52
+ "frame_duration_ms": hi["frame_duration_ms"],
53
+ "hitch_duration_ms": hi["hitch_duration_ms"],
54
+ },
55
+ tp_index=tp_index,
56
+ sui_events=sui_events,
57
+ top_symbols=top_symbols,
58
+ )
59
+ )
60
+
61
+ return correlations
62
+
63
+
64
+ # --- Internal -------------------------------------------------------------
65
+
66
+ def _build_time_profile_index(tp: dict | None):
67
+ if not tp or not tp.get("available"):
68
+ return None
69
+ samples = tp.get("_samples") or []
70
+ if not samples:
71
+ return None
72
+ # Samples are already sorted by time in time_profiler.analyze.
73
+ times = [s["time_ns"] for s in samples]
74
+ return {"times": times, "samples": samples}
75
+
76
+
77
+ def _correlate_event(
78
+ trigger_lane: str,
79
+ start_ns: int,
80
+ end_ns: int,
81
+ extra: dict,
82
+ tp_index: dict | None,
83
+ sui_events: list[dict] | None,
84
+ top_symbols: int,
85
+ ) -> dict[str, Any]:
86
+ entry: dict[str, Any] = {
87
+ "trigger": {
88
+ "lane": trigger_lane,
89
+ "start_ms": round(start_ns / 1_000_000, 2),
90
+ "end_ms": round(end_ns / 1_000_000, 2),
91
+ "duration_ms": round((end_ns - start_ns) / 1_000_000, 2),
92
+ **extra,
93
+ },
94
+ }
95
+
96
+ if tp_index is not None:
97
+ tp = _time_profile_hot_symbols(
98
+ tp_index, start_ns, end_ns, top_symbols
99
+ )
100
+ duration_ns = end_ns - start_ns
101
+ # Sample rate is 1ms/sample on standard Time Profiler. If the window
102
+ # is N ms long we'd expect ~N main-thread samples if main was fully
103
+ # running; fewer means main was blocked (I/O, lock, etc.).
104
+ expected_if_running = max(1, duration_ns // 1_000_000)
105
+ coverage_pct = min(100.0, 100.0 * tp["samples_main"] / expected_if_running)
106
+ entry["time_profiler_main_thread"] = {
107
+ "samples_in_window": tp["samples_total"],
108
+ "samples_on_main": tp["samples_main"],
109
+ "main_running_coverage_pct": round(coverage_pct, 1),
110
+ "hot_symbols": tp["hot_symbols"],
111
+ }
112
+
113
+ if sui_events is not None:
114
+ sui_overlap = _swiftui_overlaps(sui_events, start_ns, end_ns)
115
+ entry["swiftui_overlapping_updates"] = sui_overlap
116
+
117
+ return entry
118
+
119
+
120
+ def _time_profile_hot_symbols(
121
+ tp_index: dict, start_ns: int, end_ns: int, top_n: int
122
+ ) -> dict:
123
+ """Return main-thread hot symbols in the given window.
124
+
125
+ Hang/hitch/SwiftUI correlations are all main-thread responsiveness
126
+ problems, so worker-thread symbols are noise. We also return a coverage
127
+ metric — when main was blocked on I/O or a lock, the window will have
128
+ far fewer samples than its duration would predict, and that signal is
129
+ what tells the agent "this was blocked, not CPU-bound".
130
+ """
131
+ times = tp_index["times"]
132
+ samples = tp_index["samples"]
133
+ lo = bisect_left(times, start_ns)
134
+ hi = bisect_right(times, end_ns)
135
+ window = samples[lo:hi]
136
+ if not window:
137
+ return {"samples_total": 0, "samples_main": 0, "hot_symbols": []}
138
+
139
+ main_samples = [s for s in window if s["is_main"]]
140
+ weight_by_symbol: dict[str, int] = defaultdict(int)
141
+ count_by_symbol: dict[str, int] = defaultdict(int)
142
+ for s in main_samples:
143
+ weight_by_symbol[s["leaf_symbol"]] += s["weight_ns"]
144
+ count_by_symbol[s["leaf_symbol"]] += 1
145
+ total_weight = sum(weight_by_symbol.values()) or 1
146
+
147
+ ranked = sorted(weight_by_symbol.items(), key=lambda kv: kv[1], reverse=True)
148
+ hot = []
149
+ for symbol, weight in ranked[:top_n]:
150
+ hot.append({
151
+ "symbol": symbol,
152
+ "samples": count_by_symbol[symbol],
153
+ "weight_ms": round(weight / 1_000_000, 2),
154
+ "percent_of_main": round(100.0 * weight / total_weight, 2),
155
+ })
156
+ return {
157
+ "samples_total": len(window),
158
+ "samples_main": len(main_samples),
159
+ "hot_symbols": hot,
160
+ }
161
+
162
+
163
+ def _swiftui_overlaps(
164
+ events: list[dict], start_ns: int, end_ns: int
165
+ ) -> list[dict]:
166
+ # Events aren't guaranteed sorted by start_ns here (we sort by duration in
167
+ # swiftui.analyze). Linear scan; SwiftUI event counts are typically small.
168
+ out: list[dict] = []
169
+ for e in events:
170
+ if e["end_ns"] < start_ns or e["start_ns"] > end_ns:
171
+ continue
172
+ out.append({
173
+ "view": e["view"],
174
+ "duration_ms": e["duration_ms"],
175
+ "start_ms": e["start_ms"],
176
+ })
177
+ # Worst first.
178
+ out.sort(key=lambda x: x["duration_ms"], reverse=True)
179
+ return out[:10]
@@ -0,0 +1,291 @@
1
+ """Discovery helpers for os_log messages and os_signpost intervals.
2
+
3
+ These let an agent locate a focus window (e.g. "after the log saying X",
4
+ "during signpost Y") before running the main lane analysis.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from . import xctrace, xml_utils
12
+
13
+ OS_LOG_SCHEMA = "os-log"
14
+ OS_SIGNPOST_SCHEMA = "os-signpost"
15
+ OS_SIGNPOST_INTERVAL_SCHEMA = "os-signpost-interval"
16
+
17
+
18
+ def list_logs(
19
+ trace_path: Path,
20
+ toc_schemas: frozenset[str],
21
+ subsystem: str | None = None,
22
+ category: str | None = None,
23
+ message_contains: str | None = None,
24
+ message_type: str | None = None,
25
+ limit: int | None = None,
26
+ window_ns: tuple[int, int] | None = None,
27
+ run: int = 1,
28
+ ) -> list[dict[str, Any]]:
29
+ """Return os_log entries, optionally filtered. Case-insensitive contains.
30
+
31
+ `limit` counts *post-filter* matches — including the window filter — so
32
+ the caller gets N matching logs inside the window rather than the first
33
+ N matching logs that might all fall outside it.
34
+ """
35
+ if OS_LOG_SCHEMA not in toc_schemas:
36
+ return []
37
+ xml_bytes = xctrace.export_schema(trace_path, OS_LOG_SCHEMA, run=run)
38
+ stream = xml_utils.RowStream(xml_bytes)
39
+ needle = message_contains.lower() if message_contains else None
40
+
41
+ out: list[dict[str, Any]] = []
42
+ for row in stream:
43
+ time_el = row.get("time")
44
+ if time_el is None:
45
+ continue
46
+ time_ns = xml_utils.int_text(stream.resolve(time_el))
47
+ if time_ns is None:
48
+ continue
49
+ if not xml_utils.in_window(time_ns, window_ns):
50
+ continue
51
+
52
+ sub = _str_of(row, stream, "subsystem")
53
+ cat = _str_of(row, stream, "category")
54
+ typ = _str_of(row, stream, "message-type")
55
+ fmt = _str_of(row, stream, "format-string")
56
+ msg = _str_of(row, stream, "message") or fmt
57
+
58
+ if subsystem and (sub or "") != subsystem:
59
+ continue
60
+ if category and (cat or "") != category:
61
+ continue
62
+ if message_type and (typ or "") != message_type:
63
+ continue
64
+ if needle and needle not in (msg or "").lower() and needle not in (fmt or "").lower():
65
+ continue
66
+
67
+ process_el = row.get("process")
68
+ process = (
69
+ xml_utils.extract_process(process_el, stream).get("name")
70
+ if process_el is not None else None
71
+ )
72
+
73
+ out.append({
74
+ "time_ns": time_ns,
75
+ "time_ms": round(time_ns / 1_000_000, 3),
76
+ "type": typ,
77
+ "subsystem": sub,
78
+ "category": cat,
79
+ "process": process,
80
+ "message": msg,
81
+ "format_string": fmt,
82
+ })
83
+ if limit is not None and len(out) >= limit:
84
+ break
85
+
86
+ out.sort(key=lambda e: e["time_ns"])
87
+ return out
88
+
89
+
90
+ def list_signposts(
91
+ trace_path: Path,
92
+ toc_schemas: frozenset[str],
93
+ name_contains: str | None = None,
94
+ subsystem: str | None = None,
95
+ category: str | None = None,
96
+ window_ns: tuple[int, int] | None = None,
97
+ run: int = 1,
98
+ ) -> dict[str, list[dict[str, Any]]]:
99
+ """Return signpost intervals (paired begin/end) plus single-point events.
100
+
101
+ Shape: { "intervals": [...], "events": [...] }. Intervals have
102
+ start_ms/end_ms/duration_ms; events have a single time_ms.
103
+
104
+ Reads two complementary schemas:
105
+ * `os-signpost-interval`: already-paired intervals (this is where
106
+ user-emitted signposts like com.example.MyApp typically land).
107
+ * `os-signpost`: raw begin/end/event rows; we pair begins with ends
108
+ ourselves and fall back to point events for unpaired rows. Most
109
+ Apple-framework signposts (CloudKit, AppKit, …) live here.
110
+
111
+ Filters are AND-combined. `name_contains` is a case-insensitive substring
112
+ match. `window_ns` keeps intervals that overlap the window (not strict
113
+ containment) and point events whose timestamp falls inside it.
114
+ """
115
+ # The two signpost schemas overlap: every paired begin/end in `os-signpost`
116
+ # also shows up as a row in `os-signpost-interval`. To avoid duplicates we
117
+ # prefer the pre-paired schema for intervals and only mine `os-signpost`
118
+ # for point events (and for begin/end pairing as a fallback when the
119
+ # interval schema is missing — older traces).
120
+ intervals: list[dict[str, Any]] = []
121
+ events: list[dict[str, Any]] = []
122
+
123
+ has_intervals = OS_SIGNPOST_INTERVAL_SCHEMA in toc_schemas
124
+ if has_intervals:
125
+ intervals.extend(_read_interval_schema(trace_path, run=run))
126
+
127
+ if OS_SIGNPOST_SCHEMA in toc_schemas:
128
+ more_intervals, more_events = _read_event_schema(trace_path, run=run)
129
+ if not has_intervals:
130
+ intervals.extend(more_intervals)
131
+ events.extend(more_events)
132
+
133
+ intervals.sort(key=lambda i: i["start_ns"])
134
+ events.sort(key=lambda e: e["time_ns"])
135
+
136
+ needle = name_contains.lower() if name_contains else None
137
+
138
+ def _matches(entry: dict) -> bool:
139
+ if subsystem and (entry.get("subsystem") or "") != subsystem:
140
+ return False
141
+ if category and (entry.get("category") or "") != category:
142
+ return False
143
+ if needle and needle not in (entry.get("name") or "").lower():
144
+ return False
145
+ return True
146
+
147
+ if subsystem or category or needle:
148
+ intervals = [i for i in intervals if _matches(i)]
149
+ events = [e for e in events if _matches(e)]
150
+
151
+ if window_ns is not None:
152
+ s, e = window_ns
153
+ intervals = [
154
+ i for i in intervals
155
+ if not (i["end_ns"] < s or i["start_ns"] > e)
156
+ ]
157
+ events = [ev for ev in events if s <= ev["time_ns"] <= e]
158
+
159
+ return {"intervals": intervals, "events": events}
160
+
161
+
162
+ def _read_interval_schema(trace_path: Path, run: int = 1) -> list[dict[str, Any]]:
163
+ """Read the os-signpost-interval schema (pre-paired intervals)."""
164
+ xml_bytes = xctrace.export_schema(trace_path, OS_SIGNPOST_INTERVAL_SCHEMA, run=run)
165
+ stream = xml_utils.RowStream(xml_bytes)
166
+
167
+ out: list[dict[str, Any]] = []
168
+ for row in stream:
169
+ start_el = xml_utils.first_present(row, "start", "time")
170
+ dur_el = row.get("duration")
171
+ if start_el is None or dur_el is None:
172
+ continue
173
+ start_ns = xml_utils.int_text(stream.resolve(start_el))
174
+ dur_ns = xml_utils.int_text(stream.resolve(dur_el))
175
+ if start_ns is None or dur_ns is None:
176
+ continue
177
+ end_ns = start_ns + dur_ns
178
+
179
+ name = _str_of(row, stream, "name")
180
+ sub = _str_of(row, stream, "subsystem")
181
+ cat = _str_of(row, stream, "category")
182
+ signpost_id = _str_of(row, stream, "identifier") or _str_of(row, stream, "signpost-id")
183
+ process_el = row.get("process")
184
+ process = (
185
+ xml_utils.extract_process(process_el, stream).get("name")
186
+ if process_el is not None else None
187
+ )
188
+
189
+ out.append({
190
+ "start_ns": start_ns,
191
+ "end_ns": end_ns,
192
+ "duration_ns": dur_ns,
193
+ "start_ms": round(start_ns / 1_000_000, 3),
194
+ "end_ms": round(end_ns / 1_000_000, 3),
195
+ "duration_ms": round(dur_ns / 1_000_000, 3),
196
+ "name": name,
197
+ "subsystem": sub,
198
+ "category": cat,
199
+ "process": process,
200
+ "signpost_id": signpost_id,
201
+ })
202
+ return out
203
+
204
+
205
+ def _read_event_schema(
206
+ trace_path: Path,
207
+ run: int = 1,
208
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
209
+ """Read the os-signpost schema and pair begin/end rows into intervals."""
210
+ xml_bytes = xctrace.export_schema(trace_path, OS_SIGNPOST_SCHEMA, run=run)
211
+ stream = xml_utils.RowStream(xml_bytes)
212
+
213
+ pending: dict[tuple, dict] = {}
214
+ intervals: list[dict[str, Any]] = []
215
+ events: list[dict[str, Any]] = []
216
+
217
+ for row in stream:
218
+ time_el = xml_utils.first_present(row, "time", "start")
219
+ if time_el is None:
220
+ continue
221
+ time_ns = xml_utils.int_text(stream.resolve(time_el))
222
+ if time_ns is None:
223
+ continue
224
+
225
+ name = _str_of(row, stream, "name")
226
+ sub = _str_of(row, stream, "subsystem")
227
+ cat = _str_of(row, stream, "category")
228
+ event_type = _str_of(row, stream, "event-type") or _str_of(row, stream, "message-type")
229
+ signpost_id = _str_of(row, stream, "signpost-id") or _str_of(row, stream, "identifier")
230
+ process_el = row.get("process")
231
+ process = (
232
+ xml_utils.extract_process(process_el, stream).get("name")
233
+ if process_el is not None else None
234
+ )
235
+
236
+ key = (process, sub, cat, name, signpost_id)
237
+ etype = (event_type or "").lower()
238
+
239
+ if etype in ("begin", "interval begin", "start"):
240
+ pending[key] = {"start_ns": time_ns, "name": name,
241
+ "subsystem": sub, "category": cat,
242
+ "process": process, "signpost_id": signpost_id}
243
+ elif etype in ("end", "interval end", "stop"):
244
+ start = pending.pop(key, None)
245
+ if start is not None:
246
+ dur_ns = time_ns - start["start_ns"]
247
+ intervals.append({
248
+ **start,
249
+ "end_ns": time_ns,
250
+ "duration_ns": dur_ns,
251
+ "start_ms": round(start["start_ns"] / 1_000_000, 3),
252
+ "end_ms": round(time_ns / 1_000_000, 3),
253
+ "duration_ms": round(dur_ns / 1_000_000, 3),
254
+ })
255
+ else:
256
+ events.append(_point_event(time_ns, name, sub, cat,
257
+ process, signpost_id, event_type))
258
+ else:
259
+ events.append(_point_event(time_ns, name, sub, cat,
260
+ process, signpost_id, event_type))
261
+
262
+ # Unclosed begins are surfaced as point events so nothing is silently dropped.
263
+ for info in pending.values():
264
+ events.append(_point_event(info["start_ns"], info["name"],
265
+ info["subsystem"], info["category"],
266
+ info["process"], info["signpost_id"],
267
+ "Begin (unclosed)"))
268
+
269
+ return intervals, events
270
+
271
+
272
+ def _point_event(time_ns, name, subsystem, category, process, signpost_id, event_type):
273
+ return {
274
+ "time_ns": time_ns,
275
+ "time_ms": round(time_ns / 1_000_000, 3),
276
+ "name": name,
277
+ "subsystem": subsystem,
278
+ "category": category,
279
+ "process": process,
280
+ "signpost_id": signpost_id,
281
+ "event_type": event_type,
282
+ }
283
+
284
+
285
+ def _str_of(row, stream, key):
286
+ el = row.get(key)
287
+ if el is None:
288
+ return None
289
+ resolved = stream.resolve(el)
290
+ txt = xml_utils.str_text(resolved) or resolved.get("fmt")
291
+ return txt
@@ -0,0 +1,108 @@
1
+ """Hangs lane parser (schema `potential-hangs`).
2
+
3
+ The schema lacks inline backtraces — stacks come from Time Profiler samples
4
+ that overlap each hang's window. Correlation is done later in correlate.py.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from . import xctrace, xml_utils
12
+
13
+ PREFERRED_SCHEMAS = ("potential-hangs",)
14
+ FALLBACK_SCHEMAS = ("main-thread-hang", "hang", "hangs")
15
+
16
+
17
+ def analyze(
18
+ trace_path: Path,
19
+ toc_schemas: frozenset[str],
20
+ top_n: int = 10,
21
+ window: tuple[int, int] | None = None,
22
+ run: int = 1,
23
+ ) -> dict[str, Any]:
24
+ schema = _pick_schema(toc_schemas)
25
+ if schema is None:
26
+ return {
27
+ "lane": "hangs",
28
+ "available": False,
29
+ "notes": ["Hangs data not present in trace."],
30
+ }
31
+
32
+ xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
33
+ stream = xml_utils.RowStream(xml_bytes)
34
+
35
+ hangs: list[dict] = []
36
+ for row in stream:
37
+ start_el = row.get("start")
38
+ dur_el = row.get("duration")
39
+ type_el = row.get("hang-type")
40
+ thread_el = row.get("thread")
41
+ if start_el is None or dur_el is None:
42
+ continue
43
+ start_ns = xml_utils.int_text(stream.resolve(start_el))
44
+ duration_ns = xml_utils.int_text(stream.resolve(dur_el))
45
+ if start_ns is None or duration_ns is None:
46
+ continue
47
+ if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):
48
+ continue
49
+ hang_type = xml_utils.str_text(stream.resolve(type_el)) if type_el is not None else None
50
+ thread = xml_utils.extract_thread(thread_el, stream) if thread_el is not None else None
51
+
52
+ hangs.append({
53
+ "start_ns": start_ns,
54
+ "duration_ns": duration_ns,
55
+ "end_ns": start_ns + duration_ns,
56
+ "duration_ms": round(duration_ns / 1_000_000, 2),
57
+ "start_ms": round(start_ns / 1_000_000, 2),
58
+ "hang_type": hang_type or "Hang",
59
+ "thread": thread,
60
+ })
61
+
62
+ hangs.sort(key=lambda h: h["duration_ns"], reverse=True)
63
+
64
+ total_ms = sum(h["duration_ms"] for h in hangs)
65
+ worst = hangs[0] if hangs else None
66
+
67
+ # Severity buckets per Apple docs (Microhang: 250ms–500ms, Hang: ≥500ms).
68
+ # We bucket by raw duration so the agent can reason about it.
69
+ buckets = {"lt_250ms": 0, "250ms_1s": 0, "gt_1s": 0}
70
+ for h in hangs:
71
+ if h["duration_ms"] < 250:
72
+ buckets["lt_250ms"] += 1
73
+ elif h["duration_ms"] < 1000:
74
+ buckets["250ms_1s"] += 1
75
+ else:
76
+ buckets["gt_1s"] += 1
77
+
78
+ top_offenders = [
79
+ {
80
+ "start_ms": h["start_ms"],
81
+ "duration_ms": h["duration_ms"],
82
+ "hang_type": h["hang_type"],
83
+ "thread": (h["thread"] or {}).get("name", ""),
84
+ }
85
+ for h in hangs[:top_n]
86
+ ]
87
+
88
+ return {
89
+ "lane": "hangs",
90
+ "available": True,
91
+ "schema_used": schema,
92
+ "metrics": {
93
+ "count": len(hangs),
94
+ "total_duration_ms": round(total_ms, 2),
95
+ "worst_duration_ms": worst["duration_ms"] if worst else 0,
96
+ "severity_buckets": buckets,
97
+ },
98
+ "top_offenders": top_offenders,
99
+ "notes": [],
100
+ "_events": hangs, # retained for correlation
101
+ }
102
+
103
+
104
+ def _pick_schema(available: frozenset[str]) -> str | None:
105
+ for s in PREFERRED_SCHEMAS + FALLBACK_SCHEMAS:
106
+ if s in available:
107
+ return s
108
+ return None