playwright-locator-healer 0.1.3

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 (137) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/USAGE.md +217 -0
  5. package/dist/healing/audit-log.d.ts +9 -0
  6. package/dist/healing/audit-log.d.ts.map +1 -0
  7. package/dist/healing/audit-log.js +22 -0
  8. package/dist/healing/audit-log.js.map +1 -0
  9. package/dist/healing/auto-fixture.d.ts +14 -0
  10. package/dist/healing/auto-fixture.d.ts.map +1 -0
  11. package/dist/healing/auto-fixture.js +430 -0
  12. package/dist/healing/auto-fixture.js.map +1 -0
  13. package/dist/healing/cache.d.ts +11 -0
  14. package/dist/healing/cache.d.ts.map +1 -0
  15. package/dist/healing/cache.js +128 -0
  16. package/dist/healing/cache.js.map +1 -0
  17. package/dist/healing/circuit-breaker.d.ts +11 -0
  18. package/dist/healing/circuit-breaker.d.ts.map +1 -0
  19. package/dist/healing/circuit-breaker.js +34 -0
  20. package/dist/healing/circuit-breaker.js.map +1 -0
  21. package/dist/healing/cost-tracker.d.ts +30 -0
  22. package/dist/healing/cost-tracker.d.ts.map +1 -0
  23. package/dist/healing/cost-tracker.js +101 -0
  24. package/dist/healing/cost-tracker.js.map +1 -0
  25. package/dist/healing/dom-extractor.d.ts +9 -0
  26. package/dist/healing/dom-extractor.d.ts.map +1 -0
  27. package/dist/healing/dom-extractor.js +268 -0
  28. package/dist/healing/dom-extractor.js.map +1 -0
  29. package/dist/healing/errors.d.ts +33 -0
  30. package/dist/healing/errors.d.ts.map +1 -0
  31. package/dist/healing/errors.js +40 -0
  32. package/dist/healing/errors.js.map +1 -0
  33. package/dist/healing/heal/caller-context.d.ts +8 -0
  34. package/dist/healing/heal/caller-context.d.ts.map +1 -0
  35. package/dist/healing/heal/caller-context.js +46 -0
  36. package/dist/healing/heal/caller-context.js.map +1 -0
  37. package/dist/healing/heal/orchestrator-helpers.d.ts +84 -0
  38. package/dist/healing/heal/orchestrator-helpers.d.ts.map +1 -0
  39. package/dist/healing/heal/orchestrator-helpers.js +117 -0
  40. package/dist/healing/heal/orchestrator-helpers.js.map +1 -0
  41. package/dist/healing/heal/orchestrator.d.ts +47 -0
  42. package/dist/healing/heal/orchestrator.d.ts.map +1 -0
  43. package/dist/healing/heal/orchestrator.js +644 -0
  44. package/dist/healing/heal/orchestrator.js.map +1 -0
  45. package/dist/healing/heal/playwright-code.d.ts +26 -0
  46. package/dist/healing/heal/playwright-code.d.ts.map +1 -0
  47. package/dist/healing/heal/playwright-code.js +134 -0
  48. package/dist/healing/heal/playwright-code.js.map +1 -0
  49. package/dist/healing/heal/preflight.d.ts +8 -0
  50. package/dist/healing/heal/preflight.d.ts.map +1 -0
  51. package/dist/healing/heal/preflight.js +77 -0
  52. package/dist/healing/heal/preflight.js.map +1 -0
  53. package/dist/healing/heal/prompt.d.ts +9 -0
  54. package/dist/healing/heal/prompt.d.ts.map +1 -0
  55. package/dist/healing/heal/prompt.js +22 -0
  56. package/dist/healing/heal/prompt.js.map +1 -0
  57. package/dist/healing/heal/report.d.ts +11 -0
  58. package/dist/healing/heal/report.d.ts.map +1 -0
  59. package/dist/healing/heal/report.js +28 -0
  60. package/dist/healing/heal/report.js.map +1 -0
  61. package/dist/healing/heal/source-trace.d.ts +17 -0
  62. package/dist/healing/heal/source-trace.d.ts.map +1 -0
  63. package/dist/healing/heal/source-trace.js +59 -0
  64. package/dist/healing/heal/source-trace.js.map +1 -0
  65. package/dist/healing/index.d.ts +6 -0
  66. package/dist/healing/index.d.ts.map +1 -0
  67. package/dist/healing/index.js +3 -0
  68. package/dist/healing/index.js.map +1 -0
  69. package/dist/healing/llm-client.d.ts +58 -0
  70. package/dist/healing/llm-client.d.ts.map +1 -0
  71. package/dist/healing/llm-client.js +258 -0
  72. package/dist/healing/llm-client.js.map +1 -0
  73. package/dist/healing/logger.d.ts +18 -0
  74. package/dist/healing/logger.d.ts.map +1 -0
  75. package/dist/healing/logger.js +38 -0
  76. package/dist/healing/logger.js.map +1 -0
  77. package/dist/healing/models.d.ts +26 -0
  78. package/dist/healing/models.d.ts.map +1 -0
  79. package/dist/healing/models.js +109 -0
  80. package/dist/healing/models.js.map +1 -0
  81. package/dist/healing/overlay/bridge.d.ts +46 -0
  82. package/dist/healing/overlay/bridge.d.ts.map +1 -0
  83. package/dist/healing/overlay/bridge.js +2 -0
  84. package/dist/healing/overlay/bridge.js.map +1 -0
  85. package/dist/healing/overlay/dock.css.d.ts +2 -0
  86. package/dist/healing/overlay/dock.css.d.ts.map +1 -0
  87. package/dist/healing/overlay/dock.css.js +448 -0
  88. package/dist/healing/overlay/dock.css.js.map +1 -0
  89. package/dist/healing/overlay/dock.html.d.ts +46 -0
  90. package/dist/healing/overlay/dock.html.d.ts.map +1 -0
  91. package/dist/healing/overlay/dock.html.js +248 -0
  92. package/dist/healing/overlay/dock.html.js.map +1 -0
  93. package/dist/healing/overlay/drag.d.ts +17 -0
  94. package/dist/healing/overlay/drag.d.ts.map +1 -0
  95. package/dist/healing/overlay/drag.js +68 -0
  96. package/dist/healing/overlay/drag.js.map +1 -0
  97. package/dist/healing/overlay/inject.d.ts +41 -0
  98. package/dist/healing/overlay/inject.d.ts.map +1 -0
  99. package/dist/healing/overlay/inject.js +277 -0
  100. package/dist/healing/overlay/inject.js.map +1 -0
  101. package/dist/healing/overlay/keybinds.d.ts +33 -0
  102. package/dist/healing/overlay/keybinds.d.ts.map +1 -0
  103. package/dist/healing/overlay/keybinds.js +105 -0
  104. package/dist/healing/overlay/keybinds.js.map +1 -0
  105. package/dist/healing/prompts/heal-v21.d.ts +8 -0
  106. package/dist/healing/prompts/heal-v21.d.ts.map +1 -0
  107. package/dist/healing/prompts/heal-v21.js +204 -0
  108. package/dist/healing/prompts/heal-v21.js.map +1 -0
  109. package/dist/healing/retry-policy.d.ts +37 -0
  110. package/dist/healing/retry-policy.d.ts.map +1 -0
  111. package/dist/healing/retry-policy.js +46 -0
  112. package/dist/healing/retry-policy.js.map +1 -0
  113. package/dist/healing/session-cost.d.ts +44 -0
  114. package/dist/healing/session-cost.d.ts.map +1 -0
  115. package/dist/healing/session-cost.js +95 -0
  116. package/dist/healing/session-cost.js.map +1 -0
  117. package/dist/healing/test-fixture.d.ts +9 -0
  118. package/dist/healing/test-fixture.d.ts.map +1 -0
  119. package/dist/healing/test-fixture.js +37 -0
  120. package/dist/healing/test-fixture.js.map +1 -0
  121. package/dist/healing/types.d.ts +399 -0
  122. package/dist/healing/types.d.ts.map +1 -0
  123. package/dist/healing/types.js +162 -0
  124. package/dist/healing/types.js.map +1 -0
  125. package/dist/healing/validator.d.ts +64 -0
  126. package/dist/healing/validator.d.ts.map +1 -0
  127. package/dist/healing/validator.js +286 -0
  128. package/dist/healing/validator.js.map +1 -0
  129. package/dist/scripts/heal-report.d.ts +23 -0
  130. package/dist/scripts/heal-report.d.ts.map +1 -0
  131. package/dist/scripts/heal-report.js +106 -0
  132. package/dist/scripts/heal-report.js.map +1 -0
  133. package/dist/scripts/init.d.ts +3 -0
  134. package/dist/scripts/init.d.ts.map +1 -0
  135. package/dist/scripts/init.js +132 -0
  136. package/dist/scripts/init.js.map +1 -0
  137. package/package.json +84 -0
@@ -0,0 +1,204 @@
1
+ // healing/prompts/heal-v21.ts
2
+ //
3
+ // Static system prompt for the heal v2.1 LLM call. Byte-identical across all
4
+ // calls and intentionally ≥1024 tokens so OpenAI's prompt cache (75% discount
5
+ // on cached input tokens for gpt-4.1-mini) reliably hits.
6
+ //
7
+ // Keep this in its own module so the long string does not bloat llm-client.ts
8
+ // and so prompt iteration can ship without re-reading the OpenAI wrapper.
9
+ /**
10
+ * Senior-SDET persona + strict tier rubric used for every
11
+ * `requestHealedLocator` call. Mutating this string invalidates OpenAI's
12
+ * prompt-cache hash and momentarily increases per-call cost until the new
13
+ * prefix re-warms.
14
+ */
15
+ export const HEAL_V21_SYSTEM_PROMPT = `# WHO YOU ARE
16
+ You are a senior SDET architect with 8+ years writing Playwright and
17
+ Selenium tests at production scale. You have been burned by every UI
18
+ framework footgun — React reconciliation ids, Emotion / styled-
19
+ components hashed class names, MUI \`css-1k4n5\`, Vue \`data-v-{hash}\`,
20
+ Angular \`_ngcontent-c12\`, Ember UUIDs, React 18 \`:r0:\` useId,
21
+ Salesforce LWC scoped classes. You instinctively distrust anything that
22
+ looks generated and instinctively trust ARIA roles, accessible names,
23
+ and test ids. You write the locator a careful human would write by
24
+ hand — not the locator a tool would auto-export.
25
+
26
+ # WHAT YOU DO (scope of work)
27
+ ONE thing only: take the developer's picked element + their intent
28
+ description, and produce exactly 3 ranked Playwright locators in JSON.
29
+ That is all. You do not refactor tests. You do not suggest test design
30
+ changes. You do not explain your reasoning at length. You do not output
31
+ anything that is not strict JSON matching the required schema.
32
+
33
+ # WHAT IS IN SCOPE
34
+ - Generating ARIA-role, test-id, label, text, attribute-CSS, xpath, or
35
+ Playwright filter-chain locators that uniquely match the picked
36
+ element on the current page.
37
+ - Drilling DOWN when the picked element is a container — your locator
38
+ must resolve to the inner element matching the user's intent verb.
39
+ - Drilling UP when the picked element is a non-interactive descendant
40
+ (a \`<span>\` / \`<svg>\` / \`<i>\` inside a \`<button>\` / \`<a>\`) AND the
41
+ developer's intent describes the interactive ancestor — target the
42
+ clickable ancestor.
43
+
44
+ # WHAT IS OUT OF SCOPE (do not do these)
45
+ - Inventing role names, attribute values, or text that does not appear
46
+ in the picked element or candidate list. NO HALLUCINATION.
47
+ - Proposing any locator with auto-generated tokens. If a token has the
48
+ shape \`<prefix>-<digits/hex>\` with 5+ alphanumeric chars after a
49
+ hyphen or underscore, it is auto-generated. Reject it.
50
+ - Producing positional xpath (\`//div[3]/section[2]/button[1]\`) or deep
51
+ descendant CSS chains (\`body > div > main > div > section > button\`)
52
+ before round 3.
53
+ - Producing \`page.evaluate(...)\` or JavaScript expressions unless round
54
+ is exactly 4.
55
+ - Returning more or fewer than 3 suggestions.
56
+ - Returning prose, commentary, markdown fences, or anything outside the
57
+ JSON object.
58
+
59
+ # HOW YOU THINK (silent 3-step internal monologue — do NOT emit)
60
+
61
+ Step 1. UNDERSTAND
62
+ Restate the developer's intent in one sentence. What element are
63
+ they acting on? What is the verb (click, fill, expect)? If the
64
+ intent words and the picked element disagree (intent says "submit"
65
+ but pick is a Cancel button), THE PICK WINS. Note the disagreement
66
+ in your rationale.
67
+
68
+ Step 2. ANCHOR
69
+ Walk the picked element's properties in this priority order. Stop
70
+ at the FIRST strong stable anchor:
71
+ a. dataTestId present and not auto-generated → use it.
72
+ b. accessibleName non-empty AND (explicit role OR implicit role
73
+ from the table below) → use role + name.
74
+ c. associated <label> text for form fields → use label.
75
+ d. unique visible text (siblingsSameTag === 1 AND no other
76
+ candidate has the same accessibleName) → use text.
77
+ e. stable id (does not match auto-gen pattern) → use #id.
78
+ f. stable attribute (name, type, data-action / data-event /
79
+ data-name — NOT data-v / data-reactid / data-emotion).
80
+ If NONE hold, you are in fragile territory: announce in rationale
81
+ and fall back to tier 4–6.
82
+
83
+ Step 3. SCOPE
84
+ Ask: does my chosen anchor uniquely identify THE picked element on
85
+ the current page? Check siblingsSameTag and candidate count.
86
+ → If YES, emit the bare semantic locator (tier 1 or 2).
87
+ → If NO, wrap it in a filter-chain (tier 3). Pick a parent
88
+ landmark visible in parentRole (form, navigation, dialog,
89
+ region, main, listitem, table). Add \`filter({hasText: 'X'})\`
90
+ using a phrase that comes from the intent or visible sibling
91
+ text — never invented.
92
+
93
+ # WHAT YOU KNOW (skill)
94
+
95
+ ## Implicit ARIA roles (use when no explicit role attribute)
96
+ <button> → button
97
+ <a href> → link
98
+ <input type=text> → textbox
99
+ <input type=submit> → button
100
+ <input type=checkbox> → checkbox
101
+ <input type=radio> → radio
102
+ <select> → combobox
103
+ <textarea> → textbox
104
+ <h1>..<h6> → heading
105
+ <nav> → navigation
106
+ <main> → main
107
+ <header> → banner
108
+ <footer> → contentinfo
109
+ <aside> → complementary
110
+ <ul>/<ol> → list
111
+ <li> → listitem
112
+ <table> → table
113
+ <tr> → row
114
+ <td> → cell
115
+ <form> → form
116
+
117
+ ## Accessible name computation (use this order)
118
+ 1. aria-labelledby (referenced element's text)
119
+ 2. aria-label attribute
120
+ 3. <label for=id> for inputs (or wrapping <label>)
121
+ 4. element's visible textContent
122
+ 5. placeholder (inputs only, weak fallback)
123
+ 6. title attribute (weakest)
124
+
125
+ ## Edge-case recognition (apply these rules without being asked)
126
+ - Pick is <span>/<svg>/<i> inside a <button>/<a> →
127
+ target the interactive ancestor. Use parentRole + nearest
128
+ accessibleName.
129
+ - Pick is a container <section>/<div>/<form> and intent verb is
130
+ click/fill/select/expect-visible → drill down to the inner
131
+ interactive element matching the intent noun.
132
+ - accessibleName empty (icon-only button) → look for aria-label or
133
+ title. If neither, the element is a11y-broken; fall to data-*
134
+ or stable id.
135
+ - Multiple elements share the same accessibleName (5 "Edit" buttons
136
+ in a table) → tier 3 filter-chain by row text from neighborText.
137
+ - Form input has no <label for=> but has placeholder → use
138
+ role=textbox + accessibleName from associatedLabel, OR scope to
139
+ enclosing form.
140
+ - Generic visible text ("Submit", "Save", "Close") appears in many
141
+ components on the page → NEVER bare text=; always scope via
142
+ filter-chain.
143
+
144
+ # OUTPUT CONTRACT
145
+ Return strict JSON: { "suggestions": [3 entries] }. Each entry:
146
+ - selectorType: "role" | "text" | "label" | "testid" | "css" |
147
+ "xpath" | "filter-chain" | "js"
148
+ - selector: the bare selector value (forms below)
149
+ - tier: integer 1-7 matching the tier table
150
+ - rationale: ONE sentence stating the tier and what disambiguates.
151
+
152
+ ## Selector forms by type
153
+ role → \`button[name="Submit"]\` (NO \`role=\` prefix in the value)
154
+ text → \`Sign in\` (visible text only)
155
+ label → \`Email address\` (label text only)
156
+ testid → \`submit-btn\` (the testid value only)
157
+ css → \`#stable-id\`, \`[name="email"]\`, \`[data-action="save"]\`
158
+ xpath → \`//button[@data-x="y"]\` (attribute-anchored only)
159
+ filter-chain → JSON: {"root":"role=...","filters":[{"hasText":"..."}],"target":"role=..."}
160
+ js → JS expression returning ONE Element (round 4 only, never cached)
161
+
162
+ ## Tier rubric (the tier you emit is a CONSEQUENCE of ANCHOR + SCOPE)
163
+ 1 = clean semantic (role+name / label / text) — anchor unique
164
+ 2 = testid — dataTestId present and clean
165
+ 3 = filter-chain — semantic anchor exists but multi-matches → scope
166
+ 4 = attribute CSS / stable id
167
+ 5 = class chain CSS (brittle — last CSS resort)
168
+ 6 = attr-anchored xpath
169
+ 7 = js evaluate (round 4 escape hatch, never cached)
170
+
171
+ ## Round semantics
172
+ Round 1: best single-shot. Allowed tiers: 1-4.
173
+ Round 2: previous attempts failed — read previousAttempts, avoid
174
+ repeats. Allowed tiers: 1-5. Strongly prefer chained scope.
175
+ Round 3: allow tier 6 (attribute-anchored xpath).
176
+ Round 4: allow tier 7 (js evaluate).
177
+
178
+ ## Quoting (multi-word names break without quotes)
179
+ CORRECT: role=region[name="Login form"]
180
+ CORRECT: button[name="Sign in"]
181
+ WRONG: role=region[name=Login form] ← will not parse
182
+
183
+ # WORKED EXAMPLE (shows ANCHOR + SCOPE producing tier 1)
184
+ Picked: <button class="x_a82">Sign in</button> inside <form aria-label="Login form">
185
+ siblingsSameTag: 1, accessibleName: "Sign in", parentRole: "form"
186
+ Intent: "sign-in button in the login form"
187
+ Round: 1
188
+ Previous: []
189
+ Silent reasoning: UNDERSTAND → click the sign-in button.
190
+ ANCHOR → no testid, but accessibleName "Sign in" +
191
+ implicit role button.
192
+ SCOPE → siblingsSameTag=1, only one Sign in on page →
193
+ tier 1.
194
+ Output:
195
+ {"suggestions":[
196
+ {"selectorType":"role","selector":"button[name=\\"Sign in\\"]","rationale":"Tier 1: unique role+accessibleName.","tier":1},
197
+ {"selectorType":"filter-chain","selector":"{\\"root\\":\\"role=region[name=\\\\\\"Login form\\\\\\"]\\",\\"filters\\":[],\\"target\\":\\"role=button[name=\\\\\\"Sign in\\\\\\"]\\"}","rationale":"Tier 3: scoped via login-form landmark for extra robustness.","tier":3},
198
+ {"selectorType":"text","selector":"Sign in","rationale":"Tier 1 text variant.","tier":1}
199
+ ]}
200
+
201
+ # REMEMBER
202
+ Think silently. Output strict JSON only. No markdown fences. No
203
+ commentary outside JSON. No invented values.`;
204
+ //# sourceMappingURL=heal-v21.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heal-v21.js","sourceRoot":"","sources":["../../../healing/prompts/heal-v21.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,0DAA0D;AAC1D,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAE1E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CA4LO,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * healing/retry-policy.ts
3
+ *
4
+ * Exponential-backoff retry schedule for OpenAI calls. Centralised so unit
5
+ * tests can spy on the schedule and so future reliability work has one place
6
+ * to tune retry behaviour.
7
+ *
8
+ * - `BACKOFF_DELAYS_MS`: base delays (ms) for attempts 0, 1, 2. Overridable
9
+ * via the `HEALING_BACKOFF_MS` env var (`"10,20,40"`) for fast tests.
10
+ * - `backoffWithJitter(attempt)`: returns the base delay for the given
11
+ * attempt index plus ±20% random jitter, clamped at ≥0.
12
+ * - `sleep(ms)`: thin promise wrapper around `setTimeout`.
13
+ */
14
+ /**
15
+ * Base delays in milliseconds for each retry attempt (index 0, 1, 2 ...).
16
+ * Reads `HEALING_BACKOFF_MS` env at module load — comma-separated integers,
17
+ * e.g. `HEALING_BACKOFF_MS=10,20,40` for unit tests that exercise the retry
18
+ * path without slowing the suite.
19
+ */
20
+ export declare const BACKOFF_DELAYS_MS: number[];
21
+ /**
22
+ * Exponential backoff with ±20% jitter. Attempt indices ≥ schedule length
23
+ * clamp to the last entry (i.e. attempts past attempt 2 use the 4 s delay).
24
+ *
25
+ * @param attempt - Zero-based attempt index (0 = first retry).
26
+ * @returns Delay in milliseconds (≥0, rounded).
27
+ */
28
+ export declare function backoffWithJitter(attempt: number): number;
29
+ /**
30
+ * Promise-based `setTimeout`. Resolves after the given number of
31
+ * milliseconds elapse.
32
+ *
33
+ * @param ms - Wait duration in milliseconds.
34
+ * @returns Promise that resolves with `undefined`.
35
+ */
36
+ export declare function sleep(ms: number): Promise<void>;
37
+ //# sourceMappingURL=retry-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-policy.d.ts","sourceRoot":"","sources":["../../healing/retry-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,EAEhB,CAAC;AAEvB;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * healing/retry-policy.ts
3
+ *
4
+ * Exponential-backoff retry schedule for OpenAI calls. Centralised so unit
5
+ * tests can spy on the schedule and so future reliability work has one place
6
+ * to tune retry behaviour.
7
+ *
8
+ * - `BACKOFF_DELAYS_MS`: base delays (ms) for attempts 0, 1, 2. Overridable
9
+ * via the `HEALING_BACKOFF_MS` env var (`"10,20,40"`) for fast tests.
10
+ * - `backoffWithJitter(attempt)`: returns the base delay for the given
11
+ * attempt index plus ±20% random jitter, clamped at ≥0.
12
+ * - `sleep(ms)`: thin promise wrapper around `setTimeout`.
13
+ */
14
+ /**
15
+ * Base delays in milliseconds for each retry attempt (index 0, 1, 2 ...).
16
+ * Reads `HEALING_BACKOFF_MS` env at module load — comma-separated integers,
17
+ * e.g. `HEALING_BACKOFF_MS=10,20,40` for unit tests that exercise the retry
18
+ * path without slowing the suite.
19
+ */
20
+ export const BACKOFF_DELAYS_MS = process.env.HEALING_BACKOFF_MS
21
+ ? process.env.HEALING_BACKOFF_MS.split(',').map(Number)
22
+ : [1000, 2000, 4000];
23
+ /**
24
+ * Exponential backoff with ±20% jitter. Attempt indices ≥ schedule length
25
+ * clamp to the last entry (i.e. attempts past attempt 2 use the 4 s delay).
26
+ *
27
+ * @param attempt - Zero-based attempt index (0 = first retry).
28
+ * @returns Delay in milliseconds (≥0, rounded).
29
+ */
30
+ export function backoffWithJitter(attempt) {
31
+ const idx = Math.min(Math.max(0, attempt), BACKOFF_DELAYS_MS.length - 1);
32
+ const base = BACKOFF_DELAYS_MS[idx] ?? 1000;
33
+ const jitter = (Math.random() * 0.4 - 0.2) * base; // ±20%
34
+ return Math.max(0, Math.round(base + jitter));
35
+ }
36
+ /**
37
+ * Promise-based `setTimeout`. Resolves after the given number of
38
+ * milliseconds elapse.
39
+ *
40
+ * @param ms - Wait duration in milliseconds.
41
+ * @returns Promise that resolves with `undefined`.
42
+ */
43
+ export function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+ //# sourceMappingURL=retry-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-policy.js","sourceRoot":"","sources":["../../healing/retry-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAa,OAAO,CAAC,GAAG,CAAC,kBAAkB;IACvE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;IACvD,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEvB;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,iBAAiB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IAC5C,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO;IAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,44 @@
1
+ /** Active OpenAI model id resolved from `HEAL_MODEL`. */
2
+ export declare const ACTIVE_MODEL_ID: string;
3
+ /** USD per million fresh input tokens for the active model. */
4
+ export declare const PRICE_INPUT_FRESH_PER_M: number;
5
+ /** USD per million cached input tokens for the active model (prompt cache discount). */
6
+ export declare const PRICE_INPUT_CACHED_PER_M: number;
7
+ /** USD per million output tokens for the active model. */
8
+ export declare const PRICE_OUTPUT_PER_M: number;
9
+ /**
10
+ * Accumulate token usage from a completed OpenAI call into the session
11
+ * totals, returning the USD cost of *this* call.
12
+ *
13
+ * @param usage - The `completion.usage` object from the OpenAI SDK. The
14
+ * nested `prompt_tokens_details.cached_tokens` is used to bill cached
15
+ * tokens at the discounted rate.
16
+ * @returns USD cost contributed by this call (already added to session total).
17
+ */
18
+ export declare function accumulateSessionCost(usage: {
19
+ prompt_tokens: number;
20
+ completion_tokens: number;
21
+ prompt_tokens_details?: {
22
+ cached_tokens?: number;
23
+ } | null;
24
+ }): number;
25
+ /**
26
+ * Snapshot of accumulated session usage. Numbers are cumulative across
27
+ * every successful OpenAI call since process start (or last `resetSessionCost`).
28
+ *
29
+ * @returns Token counts, USD cost, and total call count.
30
+ */
31
+ export declare function getSessionCost(): {
32
+ prompt_tokens: number;
33
+ cached_tokens: number;
34
+ completion_tokens: number;
35
+ cost_usd: number;
36
+ call_count: number;
37
+ };
38
+ /**
39
+ * Reset every session counter to zero. Test-only — production code should
40
+ * never call this since cost ledgers must accumulate for the lifetime of
41
+ * the process.
42
+ */
43
+ export declare function resetSessionCost(): void;
44
+ //# sourceMappingURL=session-cost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cost.d.ts","sourceRoot":"","sources":["../../healing/session-cost.ts"],"names":[],"mappings":"AAiBA,yDAAyD;AACzD,eAAO,MAAM,eAAe,QAAkB,CAAC;AAE/C,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,QAAqB,CAAC;AAE1D,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,QAAsB,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,QAAsB,CAAC;AAQtD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC3D,GAAG,MAAM,CAaT;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAQA;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * healing/session-cost.ts
3
+ *
4
+ * Process-wide accumulator for OpenAI token usage and USD cost. Pricing is
5
+ * resolved once at module load from the active model (see `models.ts`); the
6
+ * `HEAL_MODEL` env can swap models before import.
7
+ *
8
+ * A `process.on('exit')` hook prints a session summary to stderr when at
9
+ * least one call ran — handy when a Playwright suite finishes and the
10
+ * developer wants a one-line bill.
11
+ *
12
+ * Reset via `resetSessionCost()` between test runs.
13
+ */
14
+ import { resolveModel } from './models.js';
15
+ const _activeModel = resolveModel();
16
+ /** Active OpenAI model id resolved from `HEAL_MODEL`. */
17
+ export const ACTIVE_MODEL_ID = _activeModel.id;
18
+ /** USD per million fresh input tokens for the active model. */
19
+ export const PRICE_INPUT_FRESH_PER_M = _activeModel.input;
20
+ /** USD per million cached input tokens for the active model (prompt cache discount). */
21
+ export const PRICE_INPUT_CACHED_PER_M = _activeModel.cached;
22
+ /** USD per million output tokens for the active model. */
23
+ export const PRICE_OUTPUT_PER_M = _activeModel.output;
24
+ let _sessionPromptTokens = 0;
25
+ let _sessionCachedTokens = 0;
26
+ let _sessionCompletionTokens = 0;
27
+ let _sessionCostUsd = 0;
28
+ let _sessionCallCount = 0;
29
+ /**
30
+ * Accumulate token usage from a completed OpenAI call into the session
31
+ * totals, returning the USD cost of *this* call.
32
+ *
33
+ * @param usage - The `completion.usage` object from the OpenAI SDK. The
34
+ * nested `prompt_tokens_details.cached_tokens` is used to bill cached
35
+ * tokens at the discounted rate.
36
+ * @returns USD cost contributed by this call (already added to session total).
37
+ */
38
+ export function accumulateSessionCost(usage) {
39
+ const cached = usage.prompt_tokens_details?.cached_tokens ?? 0;
40
+ const fresh = Math.max(0, usage.prompt_tokens - cached);
41
+ const callCost = (fresh / 1e6) * PRICE_INPUT_FRESH_PER_M +
42
+ (cached / 1e6) * PRICE_INPUT_CACHED_PER_M +
43
+ (usage.completion_tokens / 1e6) * PRICE_OUTPUT_PER_M;
44
+ _sessionPromptTokens += usage.prompt_tokens;
45
+ _sessionCachedTokens += cached;
46
+ _sessionCompletionTokens += usage.completion_tokens;
47
+ _sessionCostUsd += callCost;
48
+ _sessionCallCount++;
49
+ return callCost;
50
+ }
51
+ /**
52
+ * Snapshot of accumulated session usage. Numbers are cumulative across
53
+ * every successful OpenAI call since process start (or last `resetSessionCost`).
54
+ *
55
+ * @returns Token counts, USD cost, and total call count.
56
+ */
57
+ export function getSessionCost() {
58
+ return {
59
+ prompt_tokens: _sessionPromptTokens,
60
+ cached_tokens: _sessionCachedTokens,
61
+ completion_tokens: _sessionCompletionTokens,
62
+ cost_usd: _sessionCostUsd,
63
+ call_count: _sessionCallCount,
64
+ };
65
+ }
66
+ /**
67
+ * Reset every session counter to zero. Test-only — production code should
68
+ * never call this since cost ledgers must accumulate for the lifetime of
69
+ * the process.
70
+ */
71
+ export function resetSessionCost() {
72
+ _sessionPromptTokens = 0;
73
+ _sessionCachedTokens = 0;
74
+ _sessionCompletionTokens = 0;
75
+ _sessionCostUsd = 0;
76
+ _sessionCallCount = 0;
77
+ }
78
+ process.on('exit', () => {
79
+ if (_sessionCostUsd === 0)
80
+ return;
81
+ const cachedPct = _sessionPromptTokens > 0
82
+ ? ((_sessionCachedTokens / _sessionPromptTokens) * 100).toFixed(0)
83
+ : '0';
84
+ const wouldBeCost = (_sessionPromptTokens / 1e6) * PRICE_INPUT_FRESH_PER_M +
85
+ (_sessionCompletionTokens / 1e6) * PRICE_OUTPUT_PER_M;
86
+ process.stderr.write(`[HEAL] ─────────────────────────────────────────────────────\n` +
87
+ `[HEAL] SESSION LLM COST SUMMARY\n` +
88
+ `[HEAL] calls: ${_sessionCallCount}\n` +
89
+ `[HEAL] tokens: ${_sessionPromptTokens.toLocaleString()}in (${_sessionCachedTokens.toLocaleString()} cached, ${cachedPct}%) + ${_sessionCompletionTokens.toLocaleString()}out\n` +
90
+ `[HEAL] cost: $${_sessionCostUsd.toFixed(6)} USD\n` +
91
+ `[HEAL] would-be cost without cache: $${wouldBeCost.toFixed(6)} USD\n` +
92
+ `[HEAL] savings from prompt cache: $${(wouldBeCost - _sessionCostUsd).toFixed(6)} USD\n` +
93
+ `[HEAL] ─────────────────────────────────────────────────────\n`);
94
+ });
95
+ //# sourceMappingURL=session-cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cost.js","sourceRoot":"","sources":["../../healing/session-cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,YAAY,GAAG,YAAY,EAAE,CAAC;AAEpC,yDAAyD;AACzD,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAC,EAAE,CAAC;AAE/C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,uBAAuB,GAAG,YAAY,CAAC,KAAK,CAAC;AAE1D,wFAAwF;AACxF,MAAM,CAAC,MAAM,wBAAwB,GAAG,YAAY,CAAC,MAAM,CAAC;AAE5D,0DAA0D;AAC1D,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC,MAAM,CAAC;AAEtD,IAAI,oBAAoB,GAAG,CAAC,CAAC;AAC7B,IAAI,oBAAoB,GAAG,CAAC,CAAC;AAC7B,IAAI,wBAAwB,GAAG,CAAC,CAAC;AACjC,IAAI,eAAe,GAAG,CAAC,CAAC;AACxB,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAE1B;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAIrC;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,qBAAqB,EAAE,aAAa,IAAI,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,CAAC;IACxD,MAAM,QAAQ,GACZ,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,uBAAuB;QACvC,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,wBAAwB;QACzC,CAAC,KAAK,CAAC,iBAAiB,GAAG,GAAG,CAAC,GAAG,kBAAkB,CAAC;IACvD,oBAAoB,IAAI,KAAK,CAAC,aAAa,CAAC;IAC5C,oBAAoB,IAAI,MAAM,CAAC;IAC/B,wBAAwB,IAAI,KAAK,CAAC,iBAAiB,CAAC;IACpD,eAAe,IAAI,QAAQ,CAAC;IAC5B,iBAAiB,EAAE,CAAC;IACpB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAO5B,OAAO;QACL,aAAa,EAAE,oBAAoB;QACnC,aAAa,EAAE,oBAAoB;QACnC,iBAAiB,EAAE,wBAAwB;QAC3C,QAAQ,EAAE,eAAe;QACzB,UAAU,EAAE,iBAAiB;KAC9B,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,oBAAoB,GAAG,CAAC,CAAC;IACzB,oBAAoB,GAAG,CAAC,CAAC;IACzB,wBAAwB,GAAG,CAAC,CAAC;IAC7B,eAAe,GAAG,CAAC,CAAC;IACpB,iBAAiB,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;IACtB,IAAI,eAAe,KAAK,CAAC;QAAE,OAAO;IAClC,MAAM,SAAS,GACb,oBAAoB,GAAG,CAAC;QACtB,CAAC,CAAC,CAAC,CAAC,oBAAoB,GAAG,oBAAoB,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,CAAC,CAAC,GAAG,CAAC;IACV,MAAM,WAAW,GACf,CAAC,oBAAoB,GAAG,GAAG,CAAC,GAAG,uBAAuB;QACtD,CAAC,wBAAwB,GAAG,GAAG,CAAC,GAAG,kBAAkB,CAAC;IACxD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gEAAgE;QAC9D,mCAAmC;QACnC,oBAAoB,iBAAiB,IAAI;QACzC,oBAAoB,oBAAoB,CAAC,cAAc,EAAE,OAAO,oBAAoB,CAAC,cAAc,EAAE,YAAY,SAAS,QAAQ,wBAAwB,CAAC,cAAc,EAAE,OAAO;QAClL,qBAAqB,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;QACvD,0CAA0C,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;QACxE,0CAA0C,CAAC,WAAW,GAAG,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;QAC5F,gEAAgE,CACnE,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { expect } from '@playwright/test';
2
+ import { type HealActionInput } from './heal/orchestrator.js';
3
+ import type { HealEntryV2 } from './types.js';
4
+ type HealFn = (args: Omit<HealActionInput, 'page' | 'testFile'>) => Promise<HealEntryV2>;
5
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
6
+ heal: HealFn;
7
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
8
+ export { expect };
9
+ //# sourceMappingURL=test-fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-fixture.d.ts","sourceRoot":"","sources":["../../healing/test-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,UAAU,CAAC,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAEzF,eAAO,MAAM,IAAI;UAAuB,MAAM;wGA+B5C,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { test as base, expect } from '@playwright/test';
2
+ import { join, basename } from 'node:path';
3
+ import { healAction } from './heal/orchestrator.js';
4
+ import { cacheV2Get } from './cache.js';
5
+ import { buildLocator } from './validator.js';
6
+ export const test = base.extend({
7
+ heal: async ({ page }, use, testInfo) => {
8
+ const testFile = basename(testInfo.file);
9
+ // Explicit opt-in only. Generic `CI` env is too easy to set by accident
10
+ // (VS Code, IDE tasks, shell rc). Heal short-circuits ONLY when the
11
+ // caller sets HEAL_HEADLESS=1.
12
+ const isHeadlessMode = process.env.HEAL_HEADLESS === '1';
13
+ const fn = async (args) => {
14
+ if (isHeadlessMode) {
15
+ // HEAL_HEADLESS=1: cache-hit-or-fail. No overlay, no LLM.
16
+ const cacheDir = args.cacheDir ?? join(process.cwd(), '.healed-locators');
17
+ const cached = await cacheV2Get(cacheDir, testFile, args.originalSelector, args.action);
18
+ if (cached) {
19
+ const loc = buildLocator(page, {
20
+ selectorType: cached.selectorType,
21
+ selector: cached.healedSelector,
22
+ rationale: '',
23
+ tier: cached.tier ?? 6,
24
+ });
25
+ if ((await loc.count().catch(() => 0)) === 1)
26
+ return cached;
27
+ }
28
+ throw new Error(`[heal] HEAL_HEADLESS=1: no cached entry for "${args.originalSelector}" (action=${args.action}). ` +
29
+ `Cannot prompt user. Unset HEAL_HEADLESS to interact, or pre-seed the cache.`);
30
+ }
31
+ return healAction({ page, testFile, ...args });
32
+ };
33
+ await use(fn);
34
+ },
35
+ });
36
+ export { expect };
37
+ //# sourceMappingURL=test-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-fixture.js","sourceRoot":"","sources":["../../healing/test-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAwB,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAK9C,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAmB;IAChD,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE;QACtC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzC,wEAAwE;QACxE,oEAAoE;QACpE,+BAA+B;QAC/B,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,GAAG,CAAC;QAEzD,MAAM,EAAE,GAAW,KAAK,EAAE,IAAI,EAAE,EAAE;YAChC,IAAI,cAAc,EAAE,CAAC;gBACnB,0DAA0D;gBAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,kBAAkB,CAAC,CAAC;gBAC1E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBACxF,IAAI,MAAM,EAAE,CAAC;oBACX,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE;wBAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;wBACjC,QAAQ,EAAE,MAAM,CAAC,cAAc;wBAC/B,SAAS,EAAE,EAAE;wBACb,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC;qBACvB,CAAC,CAAC;oBACH,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;wBAAE,OAAO,MAAM,CAAC;gBAC9D,CAAC;gBACD,MAAM,IAAI,KAAK,CACb,gDAAgD,IAAI,CAAC,gBAAgB,aAAa,IAAI,CAAC,MAAM,KAAK;oBAClG,6EAA6E,CAC9E,CAAC;YACJ,CAAC;YACD,OAAO,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC;QACF,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;CACF,CAAC,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC"}