@atlassian/chippy 0.0.0-stage → 0.0.1-beta.1

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 (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -2
  3. package/bin/chippy.mjs +134 -0
  4. package/bin/pack.mjs +184 -0
  5. package/lib/avalanche.js +2570 -0
  6. package/lib/avalanche.js.map +7 -0
  7. package/lib/bootstrap.d.ts +1 -0
  8. package/lib/bootstrap.js +51 -0
  9. package/lib/bootstrap.js.map +7 -0
  10. package/lib/browser.js +15 -0
  11. package/lib/browser.js.map +7 -0
  12. package/lib/extension/avalanche.es.js +5396 -0
  13. package/lib/extension/avalanche.es.js.map +1 -0
  14. package/lib/extension/chippy-page-bridge.js +1833 -0
  15. package/lib/extension/inspect-react.js +216 -0
  16. package/lib/extension/layout-lab.js +1453 -0
  17. package/lib/extension/panel-dom.js +90 -0
  18. package/lib/extension/resolve-probes.js +342 -0
  19. package/lib/mcp-skill/LICENSE +201 -0
  20. package/lib/mcp-skill/README.md +150 -0
  21. package/lib/mcp-skill/SKILL.md +330 -0
  22. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  24. package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
  25. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  26. package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
  27. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  28. package/lib/mcp-skill/extension/background.js +323 -0
  29. package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
  30. package/lib/mcp-skill/extension/content-script.js +167 -0
  31. package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
  32. package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
  33. package/lib/mcp-skill/extension/icon-128.png +0 -0
  34. package/lib/mcp-skill/extension/icon-16.png +0 -0
  35. package/lib/mcp-skill/extension/icon-32.png +0 -0
  36. package/lib/mcp-skill/extension/icon-48.png +0 -0
  37. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  38. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  39. package/lib/mcp-skill/extension/logo.svg +14 -0
  40. package/lib/mcp-skill/extension/manifest.json +69 -0
  41. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  42. package/lib/mcp-skill/extension/panel-settings.js +191 -0
  43. package/lib/mcp-skill/extension/popup.html +41 -0
  44. package/lib/mcp-skill/extension/popup.js +80 -0
  45. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  46. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  47. package/lib/mcp-skill/package.json +1 -0
  48. package/lib/mcp-skill/references/README.md +56 -0
  49. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  50. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  51. package/lib/mcp-skill/references/bdd-router.md +310 -0
  52. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  53. package/lib/mcp-skill/references/command-output-formats.md +1075 -0
  54. package/lib/mcp-skill/references/command-surface.md +183 -0
  55. package/lib/mcp-skill/references/cynefin.md +99 -0
  56. package/lib/mcp-skill/references/debug-hook.md +161 -0
  57. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  58. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  59. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  60. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  61. package/lib/mcp-skill/references/mcp.md +122 -0
  62. package/lib/mcp-skill/references/mode-a-injection.md +68 -0
  63. package/lib/mcp-skill/references/operating-model.md +128 -0
  64. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  65. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  66. package/lib/mcp-skill/references/procedures.md +545 -0
  67. package/lib/mcp-skill/references/recipes.txt +114 -0
  68. package/lib/mcp-skill/references/stability-render.md +428 -0
  69. package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
  70. package/lib/mcp-skill/references/troubleshooting.md +54 -0
  71. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  72. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  73. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  74. package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
  75. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  76. package/lib/mcp-skill/scripts/chippy.sh +1218 -0
  77. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  78. package/lib/mcp-skill/scripts/helper-access.js +144 -0
  79. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  80. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  81. package/lib/mcp-skill/snippets/README.md +89 -0
  82. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  83. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  84. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  85. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  86. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  87. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  88. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  89. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  90. package/lib/package.json +1 -0
  91. package/lib/plugin.js +119 -0
  92. package/lib/plugin.js.map +7 -0
  93. package/lib/storybook/avalanche-panel-model.js +79 -0
  94. package/lib/storybook/brand.jsx +6 -0
  95. package/lib/storybook/connection-fallback.jsx +47 -0
  96. package/lib/storybook/constants.js +15 -0
  97. package/lib/storybook/context-card.jsx +171 -0
  98. package/lib/storybook/manager.jsx +403 -0
  99. package/lib/storybook/preset.js +7 -0
  100. package/lib/storybook/preview.js +263 -0
  101. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  102. package/lib/storybook/probe-intent.js +23 -0
  103. package/lib/storybook/setup.js +93 -0
  104. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  105. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  106. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  107. package/lib/types/avalanche/detector.d.ts +183 -0
  108. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  109. package/lib/types/avalanche/internals.d.ts +189 -0
  110. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  111. package/lib/types/avalanche/reporter.d.ts +610 -0
  112. package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
  113. package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
  114. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  115. package/lib/types/debug-render/core.d.ts +72 -0
  116. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  117. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  118. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  119. package/lib/types/package/avalanche.d.ts +6 -0
  120. package/lib/types/package/browser.d.ts +10 -0
  121. package/lib/types/package/plugin.d.ts +15 -0
  122. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  123. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  124. package/lib/types/plugins/index.d.ts +49 -0
  125. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  126. package/package.json +104 -4
@@ -0,0 +1,368 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Playwright probe surface for Chippy.
4
+ *
5
+ * Wraps the shared __CHIPPY runtime that is already injected by
6
+ * `playwright-session.js`. Probes here are AI-driven focus descriptors,
7
+ * NOT extension marker UI. No duplicate resolution, no coordinate identity.
8
+ *
9
+ * Lifecycle: acquire → resolve → inspect → measure → mutate → re-resolve.
10
+ */
11
+
12
+ const { launchSession } = require('./playwright-session');
13
+ const { TOKEN_HEADER, readHelperToken } = require('../scripts/helper-access');
14
+
15
+ const PROBES = new Map(); // sessionId → Map<probeId, Probe>
16
+ let SESSION_COUNTER = 0;
17
+ const SESSION_IDS = new WeakMap();
18
+
19
+ function sessionId(session) {
20
+ if (!session || !session.page) throw new Error('Invalid session');
21
+ let id = SESSION_IDS.get(session);
22
+ if (!id) {
23
+ SESSION_COUNTER += 1;
24
+ id = `session-${SESSION_COUNTER}`;
25
+ SESSION_IDS.set(session, id);
26
+ }
27
+ return id;
28
+ }
29
+
30
+ function probeStore(session) {
31
+ const id = sessionId(session);
32
+ if (!PROBES.has(id)) PROBES.set(id, new Map());
33
+ return PROBES.get(id);
34
+ }
35
+
36
+ function nowIso() { return new Date().toISOString(); }
37
+
38
+ function classifyConfidence(resolution) {
39
+ if (!resolution?.best) return 'none';
40
+ if (resolution.best.strict) return 'strict';
41
+ if (resolution.best.score >= 12) return 'high';
42
+ if (resolution.best.score >= 7) return 'medium';
43
+ return 'low';
44
+ }
45
+
46
+ async function ensureRuntime(page) {
47
+ await page.waitForFunction(() => !!globalThis.__CHIPPY?.captureProbeDescriptor && !!globalThis.__CHIPPY?.resolveProbeDescriptor);
48
+ }
49
+
50
+ // ─── Session ────────────────────────────────────────────────────────────────
51
+
52
+ async function launch(options = {}) {
53
+ const session = await launchSession({ ...options });
54
+ await ensureRuntime(session.page);
55
+ return session;
56
+ }
57
+
58
+ async function close(session) {
59
+ PROBES.delete(sessionId(session));
60
+ await session.browser.close();
61
+ }
62
+
63
+ async function goto(session, url, options = {}) {
64
+ await session.page.goto(url, { waitUntil: 'domcontentloaded', ...options });
65
+ await ensureRuntime(session.page);
66
+ }
67
+
68
+ async function reload(session, options = {}) {
69
+ await session.page.reload({ waitUntil: 'domcontentloaded', ...options });
70
+ await ensureRuntime(session.page);
71
+ }
72
+
73
+ // ─── Probe acquisition ──────────────────────────────────────────────────────
74
+
75
+ function defineProbe({ id, intent, descriptor }) {
76
+ if (!id) throw new Error('Probe id required');
77
+ if (!descriptor?.dom || !descriptor?.component) throw new Error('Probe descriptor required');
78
+ return { id, intent: intent || null, descriptor, resolved: null, history: [], createdAt: nowIso() };
79
+ }
80
+
81
+ function registerProbe(session, probe) {
82
+ const store = probeStore(session);
83
+ store.set(probe.id, probe);
84
+ return probe;
85
+ }
86
+
87
+ async function captureFromNode(session, locator) {
88
+ await ensureRuntime(session.page);
89
+ const handle = await locator.elementHandle();
90
+ if (!handle) throw new Error('Locator did not resolve to an element');
91
+ const result = await session.page.evaluate((node) => globalThis.__CHIPPY.captureProbeDescriptor(node), handle);
92
+ await handle.dispose();
93
+ if (!result?.ok) throw new Error(result?.error || 'captureProbeDescriptor failed');
94
+ return result.descriptor;
95
+ }
96
+
97
+ async function captureFromSelector(session, selector) {
98
+ return captureFromNode(session, session.page.locator(selector).first());
99
+ }
100
+
101
+ // ─── Probe resolution ──────────────────────────────────────────────────────
102
+
103
+ async function resolveProbe(session, probe, options = {}) {
104
+ await ensureRuntime(session.page);
105
+ const resolution = await session.page.evaluate(
106
+ ([descriptor, opts]) => globalThis.__CHIPPY.resolveProbeDescriptor(descriptor, opts || {}),
107
+ [probe.descriptor, options],
108
+ );
109
+ probe.resolved = resolution?.best
110
+ ? {
111
+ confidence: classifyConfidence(resolution),
112
+ candidateKind: resolution.best.candidateKind,
113
+ score: resolution.best.score,
114
+ matches: resolution.best.matches,
115
+ component: resolution.best.component,
116
+ dom: resolution.best.dom,
117
+ alternates: resolution.candidates.slice(1),
118
+ resolvedAt: nowIso(),
119
+ }
120
+ : null;
121
+ probe.history.push({ at: nowIso(), confidence: probe.resolved?.confidence || 'none', score: resolution?.best?.score || 0 });
122
+ return { resolution, probe };
123
+ }
124
+
125
+ async function refreshProbe(session, probeId, options = {}) {
126
+ const store = probeStore(session);
127
+ const probe = store.get(probeId);
128
+ if (!probe) throw new Error(`Probe not found: ${probeId}`);
129
+ return resolveProbe(session, probe, options);
130
+ }
131
+
132
+ function listProbes(session) {
133
+ return Array.from(probeStore(session).values());
134
+ }
135
+
136
+ function removeProbe(session, probeId) {
137
+ return probeStore(session).delete(probeId);
138
+ }
139
+
140
+ // ─── Helper to act on a resolved probe in page context ──────────────────────
141
+
142
+ async function withResolvedNode(session, probe, fn) {
143
+ if (!probe.resolved) await resolveProbe(session, probe);
144
+ if (!probe.resolved) throw new Error(`Probe ${probe.id} could not be resolved`);
145
+ return session.page.evaluate(({ descriptor, payload }) => {
146
+ const resolution = globalThis.__CHIPPY.resolveProbeDescriptor(descriptor);
147
+ if (!resolution?.best?.node) return { error: 'Probe not resolvable in current page state' };
148
+ return globalThis.__CHIPPY.__playwrightOps?.(payload, resolution.best) || { error: '__playwrightOps missing' };
149
+ }, { descriptor: probe.descriptor, payload: fn });
150
+ }
151
+
152
+ // Inject a small dispatcher into the page each call so all inspect ops share one path.
153
+ async function dispatch(session, probe, op) {
154
+ if (!probe.resolved) await resolveProbe(session, probe);
155
+ if (!probe.resolved) throw new Error(`Probe ${probe.id} could not be resolved`);
156
+ return session.page.evaluate(({ descriptor, op }) => {
157
+ const C = globalThis.__CHIPPY;
158
+ const resolution = C.resolveProbeDescriptor(descriptor);
159
+ const best = resolution?.best;
160
+ if (!best?.node) return { error: 'Probe not resolvable in current page state' };
161
+ const node = best.node;
162
+ const fiber = best.fiber || C.getClosestFiber?.(node) || null;
163
+ switch (op.type) {
164
+ case 'probeDom':
165
+ return C.evalElementDom(node, op.parentLevel || 0);
166
+ case 'probeOwners':
167
+ return C.inspectNode?.(node)?.component?.owners || [];
168
+ case 'probeOwner':
169
+ return (C.inspectNode?.(node)?.component?.owners || [])[op.ownerIndex] || null;
170
+ case 'probeProps':
171
+ return C.evalFiberProps(fiber, op.ownerIndex);
172
+ case 'probePropValue':
173
+ return C.evalFiberPropValue(fiber, op.propName, op.ownerIndex);
174
+ case 'probeHooks':
175
+ return C.evalFiberHooks(fiber);
176
+ case 'getComputedStyle': {
177
+ const target = op.parentLevel ? (() => { let el = node; for (let i = 0; i < op.parentLevel && el?.parentElement; i++) el = el.parentElement; return el; })() : node;
178
+ const cs = window.getComputedStyle(target);
179
+ const out = {};
180
+ for (const name of cs) out[name] = cs.getPropertyValue(name);
181
+ return out;
182
+ }
183
+ default:
184
+ return { error: `Unknown op: ${op.type}` };
185
+ }
186
+ }, { descriptor: probe.descriptor, op });
187
+ }
188
+
189
+ // ─── Inspect ────────────────────────────────────────────────────────────────
190
+
191
+ const probeDom = (session, probe, parentLevel = 0) => dispatch(session, probe, { type: 'probeDom', parentLevel });
192
+ const probeOwners = (session, probe) => dispatch(session, probe, { type: 'probeOwners' });
193
+ const probeOwner = (session, probe, ownerIndex) => dispatch(session, probe, { type: 'probeOwner', ownerIndex });
194
+ const probeProps = (session, probe, ownerIndex) => dispatch(session, probe, { type: 'probeProps', ownerIndex });
195
+ const probePropValue = (session, probe, propName, ownerIndex) => dispatch(session, probe, { type: 'probePropValue', propName, ownerIndex });
196
+ const probeHooks = (session, probe) => dispatch(session, probe, { type: 'probeHooks' });
197
+ const getComputedStyle = (session, probe, parentLevel = 0) => dispatch(session, probe, { type: 'getComputedStyle', parentLevel });
198
+
199
+ // ─── Measure (avalanche) ────────────────────────────────────────────────────
200
+
201
+ // Avalanche flows through the helper. Playwright session connects to the
202
+ // helper as a normal client; the helper drives `__AVALANCHE__` on the page
203
+ // via the page-bridge WS. Reads come back through helper HTTP responses.
204
+ //
205
+ // Why helper-routed and not page.evaluate:
206
+ // - one orchestrator (arming, auto-start, reload-with-avalanche)
207
+ // - one result format across extension and Playwright
208
+ // - one place to reason about cross-session capture state
209
+ //
210
+ // Read flow:
211
+ // helper HTTP → WS to client → __CHIPPY.pageBridge.evalAvalanche →
212
+ // __AVALANCHE__.<action> → result back through WS → helper HTTP body
213
+ //
214
+ // You only need the clientId for the Playwright page; helper does the rest.
215
+
216
+ const HELPER_BASE = process.env.CHIPPY_HELPER_BASE || 'http://127.0.0.1:7823/__chippy';
217
+ const HELPER_PORT = Number(new URL(HELPER_BASE).port) || 80;
218
+
219
+ function helperHeaders(headers = {}) {
220
+ const token = readHelperToken(HELPER_PORT);
221
+ return token ? { ...headers, [TOKEN_HEADER]: token } : headers;
222
+ }
223
+
224
+ async function helperFetch(path, body = {}) {
225
+ const response = await fetch(`${HELPER_BASE}${path}`, {
226
+ method: 'POST',
227
+ headers: helperHeaders({ 'content-type': 'application/json' }),
228
+ body: JSON.stringify(body),
229
+ });
230
+ const payload = await response.json().catch(() => ({}));
231
+ return { ok: response.ok && payload?.ok !== false, status: response.status, payload };
232
+ }
233
+
234
+ async function ensureClientId(session) {
235
+ if (session.clientId) return session.clientId;
236
+ await ensureRuntime(session.page);
237
+ const start = Date.now();
238
+ let id = null;
239
+ while (Date.now() - start < 8000) {
240
+ id = await session.page.evaluate(() => globalThis.__CHIPPY?.pageBridge?.getState?.()?.clientId || null);
241
+ if (id) break;
242
+ await new Promise((resolve) => setTimeout(resolve, 100));
243
+ }
244
+ if (!id) throw new Error('Playwright page did not register a clientId locally.');
245
+ // Wait until the helper has actually registered this clientId in /api/pages.
246
+ // Reload reuses the persisted clientId, but the helper needs the new socket
247
+ // to (re)register before it will accept commands for it.
248
+ while (Date.now() - start < 12000) {
249
+ const pages = await fetch(`${HELPER_BASE}/api/pages`, { headers: helperHeaders() }).then((r) => r.json()).catch(() => null);
250
+ if (pages?.pages?.some((page) => page.clientId === id)) {
251
+ session.clientId = id;
252
+ return id;
253
+ }
254
+ await new Promise((resolve) => setTimeout(resolve, 100));
255
+ }
256
+ throw new Error(`Helper never observed clientId ${id} after reload`);
257
+ }
258
+
259
+ async function avalanche(session, action, payload = {}) {
260
+ const clientId = await ensureClientId(session);
261
+ return helperFetch(`/api/pages/${clientId}/avalanche`, { action, ...payload });
262
+ }
263
+
264
+ // reloadWithAvalanche (Playwright model)
265
+ //
266
+ // Playwright owns the page. The shared __CHIPPY runtime + __AVALANCHE__ are
267
+ // installed via addInitScript by the session loader, so they exist BEFORE any
268
+ // page script runs on every reload. There is no race to win, no arming to
269
+ // orchestrate. The agent already controls the lifecycle.
270
+ //
271
+ // Therefore: just reload, wait for the runtime, settle, read.
272
+ // (Browser/extension flow uses helper arm + auto-start because it cannot
273
+ // guarantee inject ordering. PW does not need that.)
274
+ //
275
+ // Requirement: session must have been launched via launch(...) which uses
276
+ // installHelpers({ init: true }) under the hood.
277
+
278
+ async function reloadWithAvalanche(session, { settleMs = 3000, hotLimit = 12 } = {}) {
279
+ const previousClientId = session.clientId;
280
+ session.clientId = null;
281
+ await session.page.reload({ waitUntil: 'domcontentloaded' });
282
+ await ensureRuntime(session.page);
283
+ const clientId = await ensureClientId(session);
284
+ // The avalanche IIFE is re-injected by addInitScript on every navigation,
285
+ // but its "installed" state is per-page-runtime and starts at false. PW owns
286
+ // the lifecycle, so we install explicitly right after reload — no race, no
287
+ // origin-arming needed.
288
+ const install = await avalanche(session, 'install');
289
+ if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
290
+ const status = await avalanche(session, 'status');
291
+ const hot = await avalanche(session, 'hot-sources', { limit: hotLimit });
292
+ return { previousClientId, clientId, install, status, hot };
293
+ }
294
+
295
+ const avalancheStart = (session, max) => avalanche(session, 'install', {
296
+ options: {
297
+ detectionMode: 'full',
298
+ captureFibers: true,
299
+ ...(max != null ? { historySize: Math.max(100, Number(max) || 1000) } : {}),
300
+ },
301
+ });
302
+ const avalancheStop = (session) => avalanche(session, 'stop');
303
+ const avalancheStatus = (session) => avalanche(session, 'status');
304
+ const avalancheMetrics = (session) => avalanche(session, 'metrics');
305
+ const avalancheHotSources = (session, limit) => avalanche(session, 'hot-sources', limit != null ? { limit } : {});
306
+ const avalancheExport = (session, lastN) => avalanche(session, 'export', lastN != null ? { count: lastN } : {});
307
+
308
+ // ─── Compare ────────────────────────────────────────────────────────────────
309
+
310
+ async function snapshotProbe(session, probe) {
311
+ await resolveProbe(session, probe);
312
+ const dom = probe.resolved ? await probeDom(session, probe) : null;
313
+ const props = probe.resolved ? await probeProps(session, probe).catch(() => null) : null;
314
+ return {
315
+ at: nowIso(),
316
+ descriptor: probe.descriptor,
317
+ resolved: probe.resolved,
318
+ dom,
319
+ props,
320
+ };
321
+ }
322
+
323
+ function diffProbeSnapshots(a, b) {
324
+ const diff = { confidenceChanged: false, scoreDelta: 0, domChanged: false, propsChanged: false };
325
+ if (!a || !b) return { error: 'Two snapshots required' };
326
+ diff.confidenceChanged = a.resolved?.confidence !== b.resolved?.confidence;
327
+ diff.scoreDelta = (b.resolved?.score || 0) - (a.resolved?.score || 0);
328
+ diff.domChanged = JSON.stringify(a.dom || {}) !== JSON.stringify(b.dom || {});
329
+ diff.propsChanged = JSON.stringify(a.props || {}) !== JSON.stringify(b.props || {});
330
+ return diff;
331
+ }
332
+
333
+ module.exports = {
334
+ // session
335
+ launch,
336
+ close,
337
+ goto,
338
+ reload,
339
+ // probes
340
+ defineProbe,
341
+ registerProbe,
342
+ captureFromNode,
343
+ captureFromSelector,
344
+ resolveProbe,
345
+ refreshProbe,
346
+ listProbes,
347
+ removeProbe,
348
+ // inspect
349
+ probeDom,
350
+ probeOwners,
351
+ probeOwner,
352
+ probeProps,
353
+ probePropValue,
354
+ probeHooks,
355
+ getComputedStyle,
356
+ // measure
357
+ avalanche,
358
+ avalancheStart,
359
+ avalancheStop,
360
+ avalancheStatus,
361
+ avalancheMetrics,
362
+ avalancheHotSources,
363
+ avalancheExport,
364
+ reloadWithAvalanche,
365
+ // compare
366
+ snapshotProbe,
367
+ diffProbeSnapshots,
368
+ };
@@ -0,0 +1,114 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const PLAYWRIGHT_UNAVAILABLE_CODE = 'CHIPPY_PLAYWRIGHT_UNAVAILABLE';
5
+ let playwrightLoadResult = null;
6
+
7
+ function isMissingPlaywrightError(error) {
8
+ return error && error.code === 'MODULE_NOT_FOUND' && /(?:^|')playwright(?:'|$)/.test(String(error.message || ''));
9
+ }
10
+
11
+ function createPlaywrightUnavailableError(cause) {
12
+ /** @type {Error & { code?: string }} */
13
+ const error = new Error('Playwright is not installed. Chippy helper is running, but Playwright-only automation is unavailable. Install the playwright package to use this command.');
14
+ error.code = PLAYWRIGHT_UNAVAILABLE_CODE;
15
+ error.cause = cause;
16
+ return error;
17
+ }
18
+
19
+ function loadPlaywright() {
20
+ if (playwrightLoadResult) return playwrightLoadResult;
21
+ try {
22
+ playwrightLoadResult = { ok: true, module: require('playwright') };
23
+ } catch (error) {
24
+ if (!isMissingPlaywrightError(error)) throw error;
25
+ playwrightLoadResult = { ok: false, error: createPlaywrightUnavailableError(error) };
26
+ }
27
+ return playwrightLoadResult;
28
+ }
29
+
30
+ function getPlaywrightAvailability() {
31
+ const result = loadPlaywright();
32
+ return result.ok
33
+ ? { ok: true }
34
+ : { ok: false, code: PLAYWRIGHT_UNAVAILABLE_CODE, error: result.error.message };
35
+ }
36
+
37
+ const SKILL_ROOT = path.resolve(__dirname, '..');
38
+ const HELPER_SCRIPTS = [
39
+ // MUST be first. Installs an empty React DevTools hook shim BEFORE any page
40
+ // script (including react-dom) evaluates. react-dom registers its renderer
41
+ // into hook.renderers at evaluation time; without the shim being present
42
+ // first, hook.renderers stays empty and debug-render cannot find the
43
+ // ReactCurrentDispatcher ref it needs to capture hook call stacks. Result
44
+ // would be debugHook() collapsing to a flat fallback (no custom-hook
45
+ // folders, no callerLoc).
46
+ 'extension/devtools-hook-shim.js',
47
+ 'extension/inspect-react.js',
48
+ 'extension/layout-lab.js',
49
+ 'extension/resolve-probes.js',
50
+ 'extension/probe-descriptor.js',
51
+ 'extension/avalanche.iife.js',
52
+ 'extension/chippy-page-bridge.js',
53
+ ];
54
+
55
+ function readScript(relativePath) {
56
+ const scriptPath = path.join(SKILL_ROOT, relativePath);
57
+ return fs.readFileSync(scriptPath, 'utf8');
58
+ }
59
+
60
+ async function injectScript(page, relativePath) {
61
+ await page.addScriptTag({ content: readScript(relativePath) });
62
+ }
63
+
64
+ async function addInitScript(page, relativePath) {
65
+ await page.addInitScript({ content: readScript(relativePath) });
66
+ }
67
+
68
+ async function installHelpers(page, options = {}) {
69
+ const installer = options.init ? addInitScript : injectScript;
70
+ for (const script of HELPER_SCRIPTS) {
71
+ await installer(page, script);
72
+ }
73
+ }
74
+
75
+ async function resolveDescriptor(page, descriptor) {
76
+ return page.evaluate((value) => globalThis.__CHIPPY.resolveProbeDescriptor(value), descriptor);
77
+ }
78
+
79
+ async function captureDescriptor(page, selector) {
80
+ return page.evaluate((value) => {
81
+ const node = document.querySelector(value);
82
+ return globalThis.__CHIPPY.captureProbeDescriptor(node);
83
+ }, selector);
84
+ }
85
+
86
+ /**
87
+ * @param {{ browserName?: string, url?: string, descriptor?: unknown, headless?: boolean }} [options]
88
+ */
89
+ async function launchSession({ browserName = 'chromium', url, descriptor, headless = true } = {}) {
90
+ const playwright = loadPlaywright();
91
+ if (!playwright.ok) throw playwright.error;
92
+ const { chromium, webkit } = playwright.module;
93
+ const browserType = browserName === 'webkit' ? webkit : chromium;
94
+ const browser = await browserType.launch({ headless });
95
+ const context = await browser.newContext();
96
+ const page = await context.newPage();
97
+ await installHelpers(page, { init: true });
98
+ if (url) await page.goto(url, { waitUntil: 'domcontentloaded' });
99
+ const resolution = descriptor ? await resolveDescriptor(page, descriptor) : null;
100
+ return { browser, context, page, resolution };
101
+ }
102
+
103
+ module.exports = {
104
+ PLAYWRIGHT_UNAVAILABLE_CODE,
105
+ HELPER_SCRIPTS,
106
+ readScript,
107
+ injectScript,
108
+ addInitScript,
109
+ installHelpers,
110
+ captureDescriptor,
111
+ resolveDescriptor,
112
+ getPlaywrightAvailability,
113
+ launchSession,
114
+ };