perfgraph 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.
Files changed (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -0
  3. package/dist/causal/builder.d.ts +34 -0
  4. package/dist/causal/builder.d.ts.map +1 -0
  5. package/dist/causal/builder.js +132 -0
  6. package/dist/causal/builder.js.map +1 -0
  7. package/dist/causal/index.d.ts +11 -0
  8. package/dist/causal/index.d.ts.map +1 -0
  9. package/dist/causal/index.js +10 -0
  10. package/dist/causal/index.js.map +1 -0
  11. package/dist/causal/rules/js.d.ts +49 -0
  12. package/dist/causal/rules/js.d.ts.map +1 -0
  13. package/dist/causal/rules/js.js +209 -0
  14. package/dist/causal/rules/js.js.map +1 -0
  15. package/dist/causal/rules/layout.d.ts +39 -0
  16. package/dist/causal/rules/layout.d.ts.map +1 -0
  17. package/dist/causal/rules/layout.js +169 -0
  18. package/dist/causal/rules/layout.js.map +1 -0
  19. package/dist/causal/rules/lcp.d.ts +49 -0
  20. package/dist/causal/rules/lcp.d.ts.map +1 -0
  21. package/dist/causal/rules/lcp.js +250 -0
  22. package/dist/causal/rules/lcp.js.map +1 -0
  23. package/dist/causal/rules/network.d.ts +39 -0
  24. package/dist/causal/rules/network.d.ts.map +1 -0
  25. package/dist/causal/rules/network.js +140 -0
  26. package/dist/causal/rules/network.js.map +1 -0
  27. package/dist/causal/types.d.ts +142 -0
  28. package/dist/causal/types.d.ts.map +1 -0
  29. package/dist/causal/types.js +88 -0
  30. package/dist/causal/types.js.map +1 -0
  31. package/dist/cli/analyze.d.ts +33 -0
  32. package/dist/cli/analyze.d.ts.map +1 -0
  33. package/dist/cli/analyze.js +137 -0
  34. package/dist/cli/analyze.js.map +1 -0
  35. package/dist/cli/collect.d.ts +28 -0
  36. package/dist/cli/collect.d.ts.map +1 -0
  37. package/dist/cli/collect.js +125 -0
  38. package/dist/cli/collect.js.map +1 -0
  39. package/dist/cli/extract.d.ts +31 -0
  40. package/dist/cli/extract.d.ts.map +1 -0
  41. package/dist/cli/extract.js +135 -0
  42. package/dist/cli/extract.js.map +1 -0
  43. package/dist/cli/mcp.d.ts +14 -0
  44. package/dist/cli/mcp.d.ts.map +1 -0
  45. package/dist/cli/mcp.js +53 -0
  46. package/dist/cli/mcp.js.map +1 -0
  47. package/dist/cli/normalize.d.ts +58 -0
  48. package/dist/cli/normalize.d.ts.map +1 -0
  49. package/dist/cli/normalize.js +179 -0
  50. package/dist/cli/normalize.js.map +1 -0
  51. package/dist/cli/report.d.ts +33 -0
  52. package/dist/cli/report.d.ts.map +1 -0
  53. package/dist/cli/report.js +137 -0
  54. package/dist/cli/report.js.map +1 -0
  55. package/dist/cli/run.d.ts +49 -0
  56. package/dist/cli/run.d.ts.map +1 -0
  57. package/dist/cli/run.js +243 -0
  58. package/dist/cli/run.js.map +1 -0
  59. package/dist/collect/buffer-monitor.d.ts +25 -0
  60. package/dist/collect/buffer-monitor.d.ts.map +1 -0
  61. package/dist/collect/buffer-monitor.js +69 -0
  62. package/dist/collect/buffer-monitor.js.map +1 -0
  63. package/dist/collect/console.d.ts +25 -0
  64. package/dist/collect/console.d.ts.map +1 -0
  65. package/dist/collect/console.js +82 -0
  66. package/dist/collect/console.js.map +1 -0
  67. package/dist/collect/coverage.d.ts +26 -0
  68. package/dist/collect/coverage.d.ts.map +1 -0
  69. package/dist/collect/coverage.js +99 -0
  70. package/dist/collect/coverage.js.map +1 -0
  71. package/dist/collect/dom.d.ts +37 -0
  72. package/dist/collect/dom.d.ts.map +1 -0
  73. package/dist/collect/dom.js +118 -0
  74. package/dist/collect/dom.js.map +1 -0
  75. package/dist/collect/lighthouse.d.ts +41 -0
  76. package/dist/collect/lighthouse.d.ts.map +1 -0
  77. package/dist/collect/lighthouse.js +111 -0
  78. package/dist/collect/lighthouse.js.map +1 -0
  79. package/dist/collect/network.d.ts +32 -0
  80. package/dist/collect/network.d.ts.map +1 -0
  81. package/dist/collect/network.js +236 -0
  82. package/dist/collect/network.js.map +1 -0
  83. package/dist/collect/orchestrator.d.ts +23 -0
  84. package/dist/collect/orchestrator.d.ts.map +1 -0
  85. package/dist/collect/orchestrator.js +350 -0
  86. package/dist/collect/orchestrator.js.map +1 -0
  87. package/dist/collect/performance.d.ts +28 -0
  88. package/dist/collect/performance.d.ts.map +1 -0
  89. package/dist/collect/performance.js +108 -0
  90. package/dist/collect/performance.js.map +1 -0
  91. package/dist/collect/runtime.d.ts +26 -0
  92. package/dist/collect/runtime.d.ts.map +1 -0
  93. package/dist/collect/runtime.js +81 -0
  94. package/dist/collect/runtime.js.map +1 -0
  95. package/dist/collect/session.d.ts +58 -0
  96. package/dist/collect/session.d.ts.map +1 -0
  97. package/dist/collect/session.js +162 -0
  98. package/dist/collect/session.js.map +1 -0
  99. package/dist/collect/trace.d.ts +40 -0
  100. package/dist/collect/trace.d.ts.map +1 -0
  101. package/dist/collect/trace.js +127 -0
  102. package/dist/collect/trace.js.map +1 -0
  103. package/dist/collect/types.d.ts +317 -0
  104. package/dist/collect/types.d.ts.map +1 -0
  105. package/dist/collect/types.js +12 -0
  106. package/dist/collect/types.js.map +1 -0
  107. package/dist/distill/insights.d.ts +67 -0
  108. package/dist/distill/insights.d.ts.map +1 -0
  109. package/dist/distill/insights.js +317 -0
  110. package/dist/distill/insights.js.map +1 -0
  111. package/dist/distill/summaries.d.ts +71 -0
  112. package/dist/distill/summaries.d.ts.map +1 -0
  113. package/dist/distill/summaries.js +165 -0
  114. package/dist/distill/summaries.js.map +1 -0
  115. package/dist/extract/critical-path.d.ts +17 -0
  116. package/dist/extract/critical-path.d.ts.map +1 -0
  117. package/dist/extract/critical-path.js +95 -0
  118. package/dist/extract/critical-path.js.map +1 -0
  119. package/dist/extract/index.d.ts +16 -0
  120. package/dist/extract/index.d.ts.map +1 -0
  121. package/dist/extract/index.js +32 -0
  122. package/dist/extract/index.js.map +1 -0
  123. package/dist/extract/js-hotspots.d.ts +15 -0
  124. package/dist/extract/js-hotspots.d.ts.map +1 -0
  125. package/dist/extract/js-hotspots.js +22 -0
  126. package/dist/extract/js-hotspots.js.map +1 -0
  127. package/dist/extract/layout-shifts.d.ts +19 -0
  128. package/dist/extract/layout-shifts.d.ts.map +1 -0
  129. package/dist/extract/layout-shifts.js +47 -0
  130. package/dist/extract/layout-shifts.js.map +1 -0
  131. package/dist/extract/lcp-breakdown.d.ts +19 -0
  132. package/dist/extract/lcp-breakdown.d.ts.map +1 -0
  133. package/dist/extract/lcp-breakdown.js +110 -0
  134. package/dist/extract/lcp-breakdown.js.map +1 -0
  135. package/dist/extract/main-thread.d.ts +16 -0
  136. package/dist/extract/main-thread.d.ts.map +1 -0
  137. package/dist/extract/main-thread.js +41 -0
  138. package/dist/extract/main-thread.js.map +1 -0
  139. package/dist/extract/render-blocking.d.ts +16 -0
  140. package/dist/extract/render-blocking.d.ts.map +1 -0
  141. package/dist/extract/render-blocking.js +79 -0
  142. package/dist/extract/render-blocking.js.map +1 -0
  143. package/dist/extract/third-party.d.ts +15 -0
  144. package/dist/extract/third-party.d.ts.map +1 -0
  145. package/dist/extract/third-party.js +127 -0
  146. package/dist/extract/third-party.js.map +1 -0
  147. package/dist/extract/types.d.ts +183 -0
  148. package/dist/extract/types.d.ts.map +1 -0
  149. package/dist/extract/types.js +118 -0
  150. package/dist/extract/types.js.map +1 -0
  151. package/dist/index.d.ts +20 -0
  152. package/dist/index.d.ts.map +1 -0
  153. package/dist/index.js +188 -0
  154. package/dist/index.js.map +1 -0
  155. package/dist/mcp/handlers.d.ts +72 -0
  156. package/dist/mcp/handlers.d.ts.map +1 -0
  157. package/dist/mcp/handlers.js +285 -0
  158. package/dist/mcp/handlers.js.map +1 -0
  159. package/dist/mcp/progress.d.ts +37 -0
  160. package/dist/mcp/progress.d.ts.map +1 -0
  161. package/dist/mcp/progress.js +51 -0
  162. package/dist/mcp/progress.js.map +1 -0
  163. package/dist/mcp/prompts.d.ts +57 -0
  164. package/dist/mcp/prompts.d.ts.map +1 -0
  165. package/dist/mcp/prompts.js +225 -0
  166. package/dist/mcp/prompts.js.map +1 -0
  167. package/dist/mcp/resources.d.ts +48 -0
  168. package/dist/mcp/resources.d.ts.map +1 -0
  169. package/dist/mcp/resources.js +149 -0
  170. package/dist/mcp/resources.js.map +1 -0
  171. package/dist/mcp/server.d.ts +34 -0
  172. package/dist/mcp/server.d.ts.map +1 -0
  173. package/dist/mcp/server.js +452 -0
  174. package/dist/mcp/server.js.map +1 -0
  175. package/dist/mcp/types.d.ts +47 -0
  176. package/dist/mcp/types.d.ts.map +1 -0
  177. package/dist/mcp/types.js +67 -0
  178. package/dist/mcp/types.js.map +1 -0
  179. package/dist/normalize/clock.d.ts +40 -0
  180. package/dist/normalize/clock.d.ts.map +1 -0
  181. package/dist/normalize/clock.js +96 -0
  182. package/dist/normalize/clock.js.map +1 -0
  183. package/dist/normalize/dom-ir.d.ts +16 -0
  184. package/dist/normalize/dom-ir.d.ts.map +1 -0
  185. package/dist/normalize/dom-ir.js +48 -0
  186. package/dist/normalize/dom-ir.js.map +1 -0
  187. package/dist/normalize/index.d.ts +23 -0
  188. package/dist/normalize/index.d.ts.map +1 -0
  189. package/dist/normalize/index.js +118 -0
  190. package/dist/normalize/index.js.map +1 -0
  191. package/dist/normalize/lighthouse-insights.d.ts +27 -0
  192. package/dist/normalize/lighthouse-insights.d.ts.map +1 -0
  193. package/dist/normalize/lighthouse-insights.js +318 -0
  194. package/dist/normalize/lighthouse-insights.js.map +1 -0
  195. package/dist/normalize/lighthouse-ir.d.ts +25 -0
  196. package/dist/normalize/lighthouse-ir.d.ts.map +1 -0
  197. package/dist/normalize/lighthouse-ir.js +151 -0
  198. package/dist/normalize/lighthouse-ir.js.map +1 -0
  199. package/dist/normalize/network-ir.d.ts +23 -0
  200. package/dist/normalize/network-ir.d.ts.map +1 -0
  201. package/dist/normalize/network-ir.js +180 -0
  202. package/dist/normalize/network-ir.js.map +1 -0
  203. package/dist/normalize/runtime-ir.d.ts +18 -0
  204. package/dist/normalize/runtime-ir.d.ts.map +1 -0
  205. package/dist/normalize/runtime-ir.js +114 -0
  206. package/dist/normalize/runtime-ir.js.map +1 -0
  207. package/dist/normalize/trace-ir.d.ts +20 -0
  208. package/dist/normalize/trace-ir.d.ts.map +1 -0
  209. package/dist/normalize/trace-ir.js +224 -0
  210. package/dist/normalize/trace-ir.js.map +1 -0
  211. package/dist/normalize/types.d.ts +536 -0
  212. package/dist/normalize/types.d.ts.map +1 -0
  213. package/dist/normalize/types.js +242 -0
  214. package/dist/normalize/types.js.map +1 -0
  215. package/dist/output/manifest.d.ts +44 -0
  216. package/dist/output/manifest.d.ts.map +1 -0
  217. package/dist/output/manifest.js +232 -0
  218. package/dist/output/manifest.js.map +1 -0
  219. package/dist/output/toon.d.ts +19 -0
  220. package/dist/output/toon.d.ts.map +1 -0
  221. package/dist/output/toon.js +22 -0
  222. package/dist/output/toon.js.map +1 -0
  223. package/dist/output/writer.d.ts +31 -0
  224. package/dist/output/writer.d.ts.map +1 -0
  225. package/dist/output/writer.js +51 -0
  226. package/dist/output/writer.js.map +1 -0
  227. package/dist/report/analyzer.d.ts +29 -0
  228. package/dist/report/analyzer.d.ts.map +1 -0
  229. package/dist/report/analyzer.js +330 -0
  230. package/dist/report/analyzer.js.map +1 -0
  231. package/dist/report/index.d.ts +13 -0
  232. package/dist/report/index.d.ts.map +1 -0
  233. package/dist/report/index.js +12 -0
  234. package/dist/report/index.js.map +1 -0
  235. package/dist/report/markdown.d.ts +21 -0
  236. package/dist/report/markdown.d.ts.map +1 -0
  237. package/dist/report/markdown.js +182 -0
  238. package/dist/report/markdown.js.map +1 -0
  239. package/dist/report/remediations.d.ts +42 -0
  240. package/dist/report/remediations.d.ts.map +1 -0
  241. package/dist/report/remediations.js +179 -0
  242. package/dist/report/remediations.js.map +1 -0
  243. package/dist/report/scorer.d.ts +35 -0
  244. package/dist/report/scorer.d.ts.map +1 -0
  245. package/dist/report/scorer.js +72 -0
  246. package/dist/report/scorer.js.map +1 -0
  247. package/dist/report/types.d.ts +201 -0
  248. package/dist/report/types.d.ts.map +1 -0
  249. package/dist/report/types.js +149 -0
  250. package/dist/report/types.js.map +1 -0
  251. package/dist/shared/fs-utils.d.ts +12 -0
  252. package/dist/shared/fs-utils.d.ts.map +1 -0
  253. package/dist/shared/fs-utils.js +44 -0
  254. package/dist/shared/fs-utils.js.map +1 -0
  255. package/dist/shared/types.d.ts +69 -0
  256. package/dist/shared/types.d.ts.map +1 -0
  257. package/dist/shared/types.js +18 -0
  258. package/dist/shared/types.js.map +1 -0
  259. package/dist/shared/utils.d.ts +6 -0
  260. package/dist/shared/utils.d.ts.map +1 -0
  261. package/dist/shared/utils.js +9 -0
  262. package/dist/shared/utils.js.map +1 -0
  263. package/package.json +67 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Clock domain normalization utilities.
3
+ *
4
+ * Raw data from different CDP domains arrives on different clock domains.
5
+ * This module converts all timestamps to a common epoch: milliseconds
6
+ * relative to the navigation start (navigationStart).
7
+ *
8
+ * Supported clock domains:
9
+ * - cdp-monotonic-us : Chromium Tracing microsecond monotonic clock
10
+ * - cdp-monotonic-ms : CDP millisecond monotonic clock
11
+ * - epoch-ms : Standard Unix epoch in milliseconds
12
+ * - epoch-s : Standard Unix epoch in seconds
13
+ * - network-monotonic-s : Network domain monotonic seconds (with wall-time offset)
14
+ * - lighthouse-ms : Lighthouse-reported relative milliseconds
15
+ */
16
+ // ---------------------------------------------------------------------------
17
+ // Converters
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Convert a raw timestamp to milliseconds relative to navigationStart.
21
+ *
22
+ * NaN / Infinity guard: if the result is not a finite number, return 0.
23
+ */
24
+ export function toRelativeMs(raw, domain, anchor) {
25
+ let result;
26
+ switch (domain) {
27
+ case 'cdp-monotonic-us':
28
+ // Chrome tracing timestamps are in microseconds from boot
29
+ result = raw / 1000 - anchor.navigationStart;
30
+ break;
31
+ case 'cdp-monotonic-ms':
32
+ // Some CDP APIs return ms-monotonic
33
+ result = raw - anchor.navigationStart;
34
+ break;
35
+ case 'epoch-ms':
36
+ // Standard epoch milliseconds
37
+ result = raw - anchor.navigationStart;
38
+ break;
39
+ case 'epoch-s':
40
+ // Epoch seconds → multiply to ms, then shift
41
+ result = raw * 1000 - anchor.navigationStart;
42
+ break;
43
+ case 'network-monotonic-s': {
44
+ // Network events use a monotonic seconds clock anchored to a wall time.
45
+ // The wall-time offset is derived from the first request's wallTime.
46
+ const wallOffset = anchor.firstRequestWallTime ?? 0;
47
+ result =
48
+ raw * 1000 + (anchor.navigationStart - wallOffset * 1000);
49
+ break;
50
+ }
51
+ case 'lighthouse-ms':
52
+ // Lighthouse already reports relative ms
53
+ result = raw;
54
+ break;
55
+ default: {
56
+ // Exhaustiveness check (should never reach here if domain union is complete)
57
+ const _exhaustive = domain;
58
+ result = 0;
59
+ break;
60
+ }
61
+ }
62
+ // NaN / Infinity guard
63
+ if (!Number.isFinite(result)) {
64
+ return 0;
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Resolve a ClockAnchor from a RawDataBundle.
70
+ *
71
+ * navigationStart is extracted from the performance metrics (Timestamp metric)
72
+ * or falls back to 0. firstRequestWallTime is taken from the first network
73
+ * request's wallTime field if available.
74
+ */
75
+ export function resolveClockAnchor(raw) {
76
+ // Extract navigation start from performance metrics
77
+ let navigationStart = 0;
78
+ const perfData = raw.performance;
79
+ if (perfData?.metrics) {
80
+ const navStartMetric = perfData.metrics.find((m) => m.name === 'NavigationStart');
81
+ if (navStartMetric) {
82
+ navigationStart = navStartMetric.value;
83
+ }
84
+ }
85
+ // Extract first request wall time from network data
86
+ let firstRequestWallTime;
87
+ const netData = raw.network;
88
+ if (netData?.requests && netData.requests.length > 0) {
89
+ const firstReq = netData.requests[0];
90
+ if (firstReq && typeof firstReq.wallTime === 'number') {
91
+ firstRequestWallTime = firstReq.wallTime;
92
+ }
93
+ }
94
+ return { navigationStart, firstRequestWallTime };
95
+ }
96
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","sourceRoot":"","sources":["../../src/normalize/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyBH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,MAAmB,EACnB,MAAmB;IAEnB,IAAI,MAAc,CAAC;IAEnB,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,kBAAkB;YACrB,0DAA0D;YAC1D,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC;YAC7C,MAAM;QAER,KAAK,kBAAkB;YACrB,oCAAoC;YACpC,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC;YACtC,MAAM;QAER,KAAK,UAAU;YACb,8BAA8B;YAC9B,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC;YACtC,MAAM;QAER,KAAK,SAAS;YACZ,6CAA6C;YAC7C,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC;YAC7C,MAAM;QAER,KAAK,qBAAqB,CAAC,CAAC,CAAC;YAC3B,wEAAwE;YACxE,qEAAqE;YACrE,MAAM,UAAU,GAAG,MAAM,CAAC,oBAAoB,IAAI,CAAC,CAAC;YACpD,MAAM;gBACJ,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;YAC5D,MAAM;QACR,CAAC;QAED,KAAK,eAAe;YAClB,yCAAyC;YACzC,MAAM,GAAG,GAAG,CAAC;YACb,MAAM;QAER,OAAO,CAAC,CAAC,CAAC;YACR,6EAA6E;YAC7E,MAAM,WAAW,GAAU,MAAM,CAAC;YAClC,MAAM,GAAG,CAAC,CAAC;YACX,MAAM;QACR,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAkB;IACnD,oDAAoD;IACpD,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,MAAM,QAAQ,GAAG,GAAG,CAAC,WAER,CAAC;IACd,IAAI,QAAQ,EAAE,OAAO,EAAE,CAAC;QACtB,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CACpC,CAAC;QACF,IAAI,cAAc,EAAE,CAAC;YACnB,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC;QACzC,CAAC;IACH,CAAC;IAED,oDAAoD;IACpD,IAAI,oBAAwC,CAAC;IAE7C,MAAM,OAAO,GAAG,GAAG,CAAC,OAEP,CAAC;IACd,IAAI,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,QAAQ,IAAI,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACtD,oBAAoB,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,CAAC;AACnD,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DOMIR builder from DOM raw data.
3
+ *
4
+ * Converts CDP DOM snapshot data into a normalized DOMIR structure with
5
+ * stats, tag distribution, and layout shift candidate analysis.
6
+ */
7
+ import type { DOMIR } from './types.js';
8
+ import type { DomRawData } from '../collect/types.js';
9
+ /**
10
+ * Build a complete DOMIR from DOM raw data.
11
+ *
12
+ * @param dom - Raw DOM data from the DOMCollector
13
+ * @returns A fully-constructed DOMIR
14
+ */
15
+ export declare function buildDOMIR(dom: DomRawData): DOMIR;
16
+ //# sourceMappingURL=dom-ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-ir.d.ts","sourceRoot":"","sources":["../../src/normalize/dom-ir.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,KAAK,EAA6C,MAAM,YAAY,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAMtD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,KAAK,CAsCjD"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * DOMIR builder from DOM raw data.
3
+ *
4
+ * Converts CDP DOM snapshot data into a normalized DOMIR structure with
5
+ * stats, tag distribution, and layout shift candidate analysis.
6
+ */
7
+ import { DOMIRSchema } from './types.js';
8
+ // ---------------------------------------------------------------------------
9
+ // Main builder
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Build a complete DOMIR from DOM raw data.
13
+ *
14
+ * @param dom - Raw DOM data from the DOMCollector
15
+ * @returns A fully-constructed DOMIR
16
+ */
17
+ export function buildDOMIR(dom) {
18
+ // 1. Stats: pass through totalNodes, elementCount, maxDepth
19
+ // maxChildren is derived from elementDistribution values (max count)
20
+ const elementCounts = Object.values(dom.elementDistribution);
21
+ const maxChildren = elementCounts.length > 0
22
+ ? Math.max(...elementCounts)
23
+ : 0;
24
+ const stats = {
25
+ totalNodes: dom.stats.totalNodes,
26
+ elementCount: dom.stats.elementCount,
27
+ maxDepth: dom.stats.maxDepth,
28
+ maxChildren,
29
+ };
30
+ // 2. Tag distribution: convert Record<string, number> → TagCount[]
31
+ const tagDistribution = Object.entries(dom.elementDistribution).map(([tag, count]) => ({ tag, count }));
32
+ // 3. Layout shift candidates
33
+ const highComplexitySubtrees = maxChildren >= 50
34
+ ? elementCounts.filter((count) => count > maxChildren / 2).length
35
+ : 0;
36
+ const deepNesting = dom.stats.maxDepth >= 15 ? 1 : 0;
37
+ const layoutShiftCandidates = {
38
+ highComplexitySubtrees,
39
+ deepNesting,
40
+ };
41
+ const result = {
42
+ stats,
43
+ tagDistribution,
44
+ layoutShiftCandidates,
45
+ };
46
+ return result;
47
+ }
48
+ //# sourceMappingURL=dom-ir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-ir.js","sourceRoot":"","sources":["../../src/normalize/dom-ir.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIzC,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,GAAe;IACxC,4DAA4D;IAC5D,wEAAwE;IACxE,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC7D,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC;QAC1C,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC;QAC5B,CAAC,CAAC,CAAC,CAAC;IAEN,MAAM,KAAK,GAAa;QACtB,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU;QAChC,YAAY,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY;QACpC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;QAC5B,WAAW;KACZ,CAAC;IAEF,mEAAmE;IACnE,MAAM,eAAe,GAAe,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAC7E,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CACnC,CAAC;IAEF,6BAA6B;IAC7B,MAAM,sBAAsB,GAAG,WAAW,IAAI,EAAE;QAC9C,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,MAAM;QACjE,CAAC,CAAC,CAAC,CAAC;IACN,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,MAAM,qBAAqB,GAA0B;QACnD,sBAAsB;QACtB,WAAW;KACZ,CAAC;IAEF,MAAM,MAAM,GAAU;QACpB,KAAK;QACL,eAAe;QACf,qBAAqB;KACtB,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Normalize orchestrator.
3
+ *
4
+ * Entry point for the normalization pipeline. Takes a RawDataBundle from
5
+ * collection and returns a validated IRBundle with all timestamps in ms
6
+ * relative to navigationStart.
7
+ */
8
+ import type { RawDataBundle } from '../collect/types.js';
9
+ import type { IRBundle } from './types.js';
10
+ /**
11
+ * Normalize raw collected data into a validated Intermediate Representation
12
+ * bundle.
13
+ *
14
+ * Builds all domain IRs (performance, network, runtime, DOM, lighthouse)
15
+ * from their respective raw data sources and assembles them into a
16
+ * validated IRBundle.
17
+ *
18
+ * @param raw - Raw data bundle from a single collection run
19
+ * @returns Validated IRBundle
20
+ * @throws {Error} If the assembled bundle fails Zod validation
21
+ */
22
+ export declare function normalize(raw: RawDataBundle): IRBundle;
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/normalize/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAA8C,MAAM,qBAAqB,CAAC;AACrG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU3C;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,GAAG,QAAQ,CA8GtD"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Normalize orchestrator.
3
+ *
4
+ * Entry point for the normalization pipeline. Takes a RawDataBundle from
5
+ * collection and returns a validated IRBundle with all timestamps in ms
6
+ * relative to navigationStart.
7
+ */
8
+ import { IRBundleSchema } from './types.js';
9
+ import { resolveClockAnchor } from './clock.js';
10
+ import { buildPerformanceIR } from './trace-ir.js';
11
+ import { buildNetworkIR } from './network-ir.js';
12
+ import { buildRuntimeIR } from './runtime-ir.js';
13
+ import { buildDOMIR } from './dom-ir.js';
14
+ import { buildLighthouseIR } from './lighthouse-ir.js';
15
+ /**
16
+ * Normalize raw collected data into a validated Intermediate Representation
17
+ * bundle.
18
+ *
19
+ * Builds all domain IRs (performance, network, runtime, DOM, lighthouse)
20
+ * from their respective raw data sources and assembles them into a
21
+ * validated IRBundle.
22
+ *
23
+ * @param raw - Raw data bundle from a single collection run
24
+ * @returns Validated IRBundle
25
+ * @throws {Error} If the assembled bundle fails Zod validation
26
+ */
27
+ export function normalize(raw) {
28
+ // 1. Resolve the clock anchor (navigationStart time)
29
+ const anchor = resolveClockAnchor(raw);
30
+ // 2. Build PerformanceIR from trace + performance data
31
+ const traceData = raw.trace;
32
+ const perfData = raw.performance;
33
+ const performance = (traceData && perfData)
34
+ ? buildPerformanceIR(raw.trace, raw.performance, anchor)
35
+ : {
36
+ navigation: {
37
+ url: '',
38
+ navigationStart: 0,
39
+ domContentLoaded: 0,
40
+ domContentLoadedEventEnd: 0,
41
+ loadEventStart: 0,
42
+ loadEventEnd: 0,
43
+ domInteractive: 0,
44
+ },
45
+ coreWebVitals: {},
46
+ traceSummary: {
47
+ totalDuration: 0,
48
+ eventCount: 0,
49
+ categories: {},
50
+ threadActivity: { totalMs: 0, byCategory: {} },
51
+ },
52
+ mainThreadBusyness: 0,
53
+ };
54
+ // 3. Build domain IRs
55
+ const network = raw.network
56
+ ? buildNetworkIR(raw.network, anchor)
57
+ : {
58
+ requests: [],
59
+ summary: {
60
+ totalRequests: 0,
61
+ totalBytes: 0,
62
+ byType: {},
63
+ byPriority: {},
64
+ criticalPath: { tree: { url: '' }, depth: 0, urlsOnLongestPath: [] },
65
+ longestChain: { url: '', length: 0 },
66
+ },
67
+ };
68
+ const runtimeRaw = raw.runtime;
69
+ const consoleRaw = raw.consoleEntries;
70
+ const runtime = runtimeRaw
71
+ ? buildRuntimeIR(runtimeRaw, consoleRaw ?? { entries: [], counts: { log: 0, warn: 0, error: 0, info: 0, debug: 0, other: 0 }, warnings: [] })
72
+ : { executionContexts: [] };
73
+ const domRaw = raw.dom;
74
+ const dom = domRaw
75
+ ? buildDOMIR(domRaw)
76
+ : {
77
+ stats: { totalNodes: 0, elementCount: 0, maxDepth: 0, maxChildren: 0 },
78
+ tagDistribution: [],
79
+ layoutShiftCandidates: { highComplexitySubtrees: 0, deepNesting: 0 },
80
+ };
81
+ const lighthouse = raw.lighthouse
82
+ ? buildLighthouseIR(raw.lighthouse)
83
+ : {
84
+ categories: {},
85
+ failedAudits: [],
86
+ scores: {},
87
+ };
88
+ // 4. Extract URL from the first network request (most reliable source)
89
+ const networkRaw = raw.network;
90
+ const firstRequestUrl = networkRaw?.requests?.[0]?.url
91
+ ?? network.requests[0]?.url
92
+ ?? performance.navigation.url
93
+ ?? '';
94
+ // 5. Assemble IRMeta
95
+ const perfMetrics = perfData?.metrics;
96
+ const navStartMetric = perfMetrics?.find((m) => m.name === 'NavigationStart');
97
+ const meta = {
98
+ url: firstRequestUrl,
99
+ fetchedAt: new Date().toISOString(),
100
+ navigationStart: navStartMetric?.value ?? 0,
101
+ irVersion: '1.0.0',
102
+ };
103
+ // 6. Build and validate IRBundle
104
+ const bundle = {
105
+ meta,
106
+ performance,
107
+ network,
108
+ runtime,
109
+ dom,
110
+ lighthouse,
111
+ };
112
+ const parseResult = IRBundleSchema.safeParse(bundle);
113
+ if (!parseResult.success) {
114
+ throw new Error(`Error: IR validation failed: ${parseResult.error.message}`);
115
+ }
116
+ return parseResult.data;
117
+ }
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/normalize/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Lighthouse Insights builder from Lighthouse v13+ insight audits.
3
+ *
4
+ * Extracts structured data from 3 specific Lighthouse insight audits:
5
+ * - lcp-breakdown-insight
6
+ * - render-blocking-insight
7
+ * - network-dependency-tree-insight
8
+ *
9
+ * Each insight audit is parsed independently so partial data does not
10
+ * prevent the rest from being extracted.
11
+ */
12
+ import type { LighthouseInsights } from './types.js';
13
+ /**
14
+ * Build a structured LighthouseInsights object from raw Lighthouse data.
15
+ *
16
+ * Accepts raw Lighthouse data in either form:
17
+ * - `{ lhr: { audits: {...} } }` — from LighthouseRawData
18
+ * - `{ audits: {...} }` — from a plain Lighthouse Report
19
+ *
20
+ * Each insight audit is parsed independently. Missing or invalid audits
21
+ * simply result in that field being omitted from the result.
22
+ *
23
+ * @param rawLighthouse - Raw Lighthouse data (unknown shape, validated at parse time)
24
+ * @returns A partially-populated LighthouseInsights object
25
+ */
26
+ export declare function buildLighthouseInsights(rawLighthouse: unknown): LighthouseInsights;
27
+ //# sourceMappingURL=lighthouse-insights.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lighthouse-insights.d.ts","sourceRoot":"","sources":["../../src/normalize/lighthouse-insights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,YAAY,CAAC;AA+RpB;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,OAAO,GACrB,kBAAkB,CA2CpB"}
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Lighthouse Insights builder from Lighthouse v13+ insight audits.
3
+ *
4
+ * Extracts structured data from 3 specific Lighthouse insight audits:
5
+ * - lcp-breakdown-insight
6
+ * - render-blocking-insight
7
+ * - network-dependency-tree-insight
8
+ *
9
+ * Each insight audit is parsed independently so partial data does not
10
+ * prevent the rest from being extracted.
11
+ */
12
+ // ---------------------------------------------------------------------------
13
+ // Internal helpers for safe access
14
+ // ---------------------------------------------------------------------------
15
+ /** Assert value is a plain object (record-like). */
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ /** Safely coerce a value to a string. */
20
+ function safeString(value) {
21
+ return typeof value === 'string' ? value : undefined;
22
+ }
23
+ /** Safely coerce a value to a number. */
24
+ function safeNumber(value) {
25
+ return typeof value === 'number' ? value : undefined;
26
+ }
27
+ /** Safely coerce a value to an array, filtering out non-record entries. */
28
+ function safeRecordArray(value) {
29
+ if (!Array.isArray(value))
30
+ return [];
31
+ return value.filter(isRecord);
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Insight audit parsers
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Parse the lcp-breakdown-insight audit.
38
+ *
39
+ * Lighthouse v13+ structures this as a list containing:
40
+ * - a table item with subpart rows (timeToFirstByte, elementRenderDelay,
41
+ * resourceLoadDelay, resourceLoadTime)
42
+ * - a node item with LCP element metadata (selector, snippet, nodeLabel)
43
+ */
44
+ function parseLCPBreakdown(audit) {
45
+ const details = audit.details;
46
+ if (!isRecord(details))
47
+ return undefined;
48
+ const items = safeRecordArray(details.items);
49
+ if (items.length === 0)
50
+ return undefined;
51
+ let timeToFirstByte = 0;
52
+ let resourceLoadDelay;
53
+ let resourceLoadTime;
54
+ let elementRenderDelay = 0;
55
+ let lcpElementSelector;
56
+ let lcpElementSnippet;
57
+ let lcpElementNodeLabel;
58
+ for (const item of items) {
59
+ const itemType = safeString(item.type);
60
+ if (itemType === 'table') {
61
+ // Extract subpart durations from the table items
62
+ const subItems = safeRecordArray(item.items);
63
+ for (const sub of subItems) {
64
+ const subpart = safeString(sub.subpart);
65
+ const duration = safeNumber(sub.duration) ?? 0;
66
+ switch (subpart) {
67
+ case 'timeToFirstByte':
68
+ timeToFirstByte = duration;
69
+ break;
70
+ case 'elementRenderDelay':
71
+ elementRenderDelay = duration;
72
+ break;
73
+ case 'resourceLoadDelay':
74
+ resourceLoadDelay = duration;
75
+ break;
76
+ case 'resourceLoadTime':
77
+ resourceLoadTime = duration;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ if (itemType === 'node') {
83
+ lcpElementSelector = safeString(item.selector);
84
+ lcpElementSnippet = safeString(item.snippet);
85
+ lcpElementNodeLabel = safeString(item.nodeLabel);
86
+ }
87
+ }
88
+ // Determine source
89
+ const scoreDisplayMode = safeString(audit.scoreDisplayMode);
90
+ let source = 'lighthouse';
91
+ if (scoreDisplayMode === 'informative') {
92
+ // Insight audits with trace data combined are 'mixed'
93
+ source = 'mixed';
94
+ }
95
+ return {
96
+ timeToFirstByte,
97
+ elementRenderDelay,
98
+ resourceLoadDelay: resourceLoadDelay !== undefined ? resourceLoadDelay : undefined,
99
+ resourceLoadTime: resourceLoadTime !== undefined ? resourceLoadTime : undefined,
100
+ lcpElementSelector,
101
+ lcpElementSnippet,
102
+ lcpElementNodeLabel,
103
+ source,
104
+ };
105
+ }
106
+ /**
107
+ * Parse the render-blocking-insight audit.
108
+ *
109
+ * Lighthouse v13+ structures this as:
110
+ * - metricSavings: { FCP: number, LCP: number }
111
+ * - details.items: array of render-blocking resources
112
+ */
113
+ function parseRenderBlocking(audit) {
114
+ const details = audit.details;
115
+ if (!isRecord(details))
116
+ return undefined;
117
+ // Extract metric savings from the audit level
118
+ const metricSavings = isRecord(audit.metricSavings) ? audit.metricSavings : undefined;
119
+ const fcpSavingsMs = safeNumber(metricSavings?.['FCP']);
120
+ const lcpSavingsMs = safeNumber(metricSavings?.['LCP']);
121
+ // Extract resource list from details.items
122
+ const items = safeRecordArray(details.items);
123
+ const resources = [];
124
+ for (const item of items) {
125
+ const url = safeString(item.url);
126
+ if (!url)
127
+ continue;
128
+ resources.push({
129
+ url,
130
+ totalBytes: safeNumber(item.totalBytes),
131
+ wastedMs: safeNumber(item.wastedMs),
132
+ resourceType: safeString(item.resourceType),
133
+ });
134
+ }
135
+ return {
136
+ fcpSavingsMs: fcpSavingsMs,
137
+ lcpSavingsMs: lcpSavingsMs,
138
+ resources,
139
+ };
140
+ }
141
+ /**
142
+ * Recursively walk a network chain subtree to collect all leaf URLs
143
+ * along the longest path (by navStartToEndTime).
144
+ */
145
+ function walkLongestChain(node, path) {
146
+ let bestDuration = node.navStartToEndTime;
147
+ let bestUrls = [...path, node.url];
148
+ if (node.children) {
149
+ for (const childKey of Object.keys(node.children)) {
150
+ const child = node.children[childKey];
151
+ const result = walkLongestChain(child, [...path, node.url]);
152
+ const totalDuration = node.navStartToEndTime + result.duration;
153
+ if (totalDuration > bestDuration) {
154
+ bestDuration = totalDuration;
155
+ bestUrls = result.urls;
156
+ }
157
+ }
158
+ }
159
+ return { duration: bestDuration, urls: bestUrls };
160
+ }
161
+ /**
162
+ * Convert a raw chain record (unknown-shape) into a proper NetworkChainNode
163
+ * by recursively walking and casting each entry.
164
+ */
165
+ function parseChainNode(raw) {
166
+ if (!isRecord(raw))
167
+ return undefined;
168
+ const url = safeString(raw.url);
169
+ if (!url)
170
+ return undefined;
171
+ const node = {
172
+ url,
173
+ navStartToEndTime: safeNumber(raw.navStartToEndTime) ?? 0,
174
+ transferSize: safeNumber(raw.transferSize),
175
+ isLongest: raw.isLongest === true,
176
+ };
177
+ if (raw.children && isRecord(raw.children)) {
178
+ const children = {};
179
+ for (const [key, childRaw] of Object.entries(raw.children)) {
180
+ const child = parseChainNode(childRaw);
181
+ if (child) {
182
+ children[key] = child;
183
+ }
184
+ }
185
+ if (Object.keys(children).length > 0) {
186
+ node.children = children;
187
+ }
188
+ }
189
+ return node;
190
+ }
191
+ /**
192
+ * Parse the network-dependency-tree-insight audit.
193
+ *
194
+ * Lighthouse v13+ structures this as a list containing:
195
+ * - a network-tree item with chains and longestChain metadata
196
+ * - a list-section item with preconnect candidates
197
+ */
198
+ function parseNetworkDependencyTree(audit) {
199
+ const details = audit.details;
200
+ if (!isRecord(details))
201
+ return undefined;
202
+ const items = safeRecordArray(details.items);
203
+ if (items.length === 0)
204
+ return undefined;
205
+ let chains;
206
+ let longestChainDuration;
207
+ const longestChainUrls = [];
208
+ const preconnectCandidates = [];
209
+ for (const item of items) {
210
+ const itemType = safeString(item.type);
211
+ if (itemType === 'network-tree') {
212
+ // Parse chains
213
+ const rawChains = isRecord(item.chains) ? item.chains : undefined;
214
+ if (rawChains) {
215
+ const parsed = {};
216
+ for (const [key, rawNode] of Object.entries(rawChains)) {
217
+ const node = parseChainNode(rawNode);
218
+ if (node) {
219
+ parsed[key] = node;
220
+ }
221
+ }
222
+ if (Object.keys(parsed).length > 0) {
223
+ chains = parsed;
224
+ // Walk the longest chain to get URLs
225
+ for (const rootNode of Object.values(parsed)) {
226
+ const result = walkLongestChain(rootNode, []);
227
+ if (result.duration > (longestChainDuration ?? 0)) {
228
+ longestChainDuration = result.duration;
229
+ }
230
+ longestChainUrls.push(...result.urls);
231
+ }
232
+ }
233
+ }
234
+ // Extract longestChain duration from the item-level metadata
235
+ const longestChain = isRecord(item.longestChain) ? item.longestChain : undefined;
236
+ if (longestChain) {
237
+ const duration = safeNumber(longestChain.duration);
238
+ if (duration !== undefined) {
239
+ longestChainDuration = duration;
240
+ }
241
+ }
242
+ }
243
+ if (itemType === 'list-section') {
244
+ const title = safeString(item.title);
245
+ if (title === 'Preconnect candidates' || title?.toLowerCase().includes('preconnect')) {
246
+ const preconnectItems = safeRecordArray(item.items);
247
+ for (const pi of preconnectItems) {
248
+ const url = safeString(pi.url);
249
+ if (url) {
250
+ preconnectCandidates.push(url);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ return {
257
+ chains,
258
+ longestChainDuration: longestChainDuration,
259
+ longestChainUrls: longestChainUrls.length > 0 ? longestChainUrls : undefined,
260
+ preconnectCandidates: preconnectCandidates.length > 0 ? preconnectCandidates : undefined,
261
+ };
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // Public API
265
+ // ---------------------------------------------------------------------------
266
+ /**
267
+ * Build a structured LighthouseInsights object from raw Lighthouse data.
268
+ *
269
+ * Accepts raw Lighthouse data in either form:
270
+ * - `{ lhr: { audits: {...} } }` — from LighthouseRawData
271
+ * - `{ audits: {...} }` — from a plain Lighthouse Report
272
+ *
273
+ * Each insight audit is parsed independently. Missing or invalid audits
274
+ * simply result in that field being omitted from the result.
275
+ *
276
+ * @param rawLighthouse - Raw Lighthouse data (unknown shape, validated at parse time)
277
+ * @returns A partially-populated LighthouseInsights object
278
+ */
279
+ export function buildLighthouseInsights(rawLighthouse) {
280
+ if (!isRecord(rawLighthouse))
281
+ return {};
282
+ // Handle both { lhr: { audits } } and { audits } formats
283
+ const root = isRecord(rawLighthouse.lhr) ? rawLighthouse.lhr : rawLighthouse;
284
+ const audits = isRecord(root.audits) ? root.audits : {};
285
+ const insights = {};
286
+ // 1. Parse lcp-breakdown-insight
287
+ const lcpAudit = isRecord(audits['lcp-breakdown-insight'])
288
+ ? audits['lcp-breakdown-insight']
289
+ : undefined;
290
+ if (lcpAudit) {
291
+ const parsed = parseLCPBreakdown(lcpAudit);
292
+ if (parsed) {
293
+ insights.lcpBreakdown = parsed;
294
+ }
295
+ }
296
+ // 2. Parse render-blocking-insight
297
+ const rbAudit = isRecord(audits['render-blocking-insight'])
298
+ ? audits['render-blocking-insight']
299
+ : undefined;
300
+ if (rbAudit) {
301
+ const parsed = parseRenderBlocking(rbAudit);
302
+ if (parsed) {
303
+ insights.renderBlocking = parsed;
304
+ }
305
+ }
306
+ // 3. Parse network-dependency-tree-insight
307
+ const netAudit = isRecord(audits['network-dependency-tree-insight'])
308
+ ? audits['network-dependency-tree-insight']
309
+ : undefined;
310
+ if (netAudit) {
311
+ const parsed = parseNetworkDependencyTree(netAudit);
312
+ if (parsed) {
313
+ insights.networkDependencyTree = parsed;
314
+ }
315
+ }
316
+ return insights;
317
+ }
318
+ //# sourceMappingURL=lighthouse-insights.js.map