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

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 (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -2
  3. package/bin/chippy.mjs +92 -0
  4. package/bin/pack.mjs +204 -0
  5. package/bin/skill-dir.mjs +15 -0
  6. package/lib/avalanche.js +2601 -0
  7. package/lib/avalanche.js.map +7 -0
  8. package/lib/bootstrap.d.ts +1 -0
  9. package/lib/bootstrap.js +70 -0
  10. package/lib/bootstrap.js.map +7 -0
  11. package/lib/browser.js +15 -0
  12. package/lib/browser.js.map +7 -0
  13. package/lib/extension/avalanche.es.js +5535 -0
  14. package/lib/extension/avalanche.es.js.map +1 -0
  15. package/lib/extension/chippy-page-bridge.js +1953 -0
  16. package/lib/extension/inspect-react.js +216 -0
  17. package/lib/extension/layout-lab.js +1453 -0
  18. package/lib/extension/panel-dom.js +90 -0
  19. package/lib/extension/resolve-probes.js +342 -0
  20. package/lib/mcp-skill/LICENSE +201 -0
  21. package/lib/mcp-skill/README.md +148 -0
  22. package/lib/mcp-skill/SKILL.md +335 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  24. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  25. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  26. package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
  27. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  28. package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
  29. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  30. package/lib/mcp-skill/extension/background.js +325 -0
  31. package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
  32. package/lib/mcp-skill/extension/content-script.js +190 -0
  33. package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
  34. package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
  35. package/lib/mcp-skill/extension/icon-128.png +0 -0
  36. package/lib/mcp-skill/extension/icon-16.png +0 -0
  37. package/lib/mcp-skill/extension/icon-32.png +0 -0
  38. package/lib/mcp-skill/extension/icon-48.png +0 -0
  39. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  40. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  41. package/lib/mcp-skill/extension/logo.svg +14 -0
  42. package/lib/mcp-skill/extension/manifest.json +69 -0
  43. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  44. package/lib/mcp-skill/extension/panel-settings.js +173 -0
  45. package/lib/mcp-skill/extension/popup.html +41 -0
  46. package/lib/mcp-skill/extension/popup.js +80 -0
  47. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  48. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  49. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  50. package/lib/mcp-skill/package.json +1 -0
  51. package/lib/mcp-skill/references/README.md +56 -0
  52. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  53. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  54. package/lib/mcp-skill/references/bdd-router.md +310 -0
  55. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  56. package/lib/mcp-skill/references/command-output-formats.md +1085 -0
  57. package/lib/mcp-skill/references/command-surface.md +183 -0
  58. package/lib/mcp-skill/references/cynefin.md +99 -0
  59. package/lib/mcp-skill/references/debug-hook.md +161 -0
  60. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  61. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  62. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  63. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  64. package/lib/mcp-skill/references/mcp.md +122 -0
  65. package/lib/mcp-skill/references/mode-a-injection.md +102 -0
  66. package/lib/mcp-skill/references/operating-model.md +128 -0
  67. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  68. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  69. package/lib/mcp-skill/references/procedures.md +545 -0
  70. package/lib/mcp-skill/references/recipes.txt +114 -0
  71. package/lib/mcp-skill/references/stability-render.md +428 -0
  72. package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
  73. package/lib/mcp-skill/references/troubleshooting.md +59 -0
  74. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  75. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  76. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  77. package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
  78. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  79. package/lib/mcp-skill/scripts/chippy.sh +1252 -0
  80. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  81. package/lib/mcp-skill/scripts/helper-access.js +158 -0
  82. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  83. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  84. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  85. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  86. package/lib/mcp-skill/snippets/README.md +89 -0
  87. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  88. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  89. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  90. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  91. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  92. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  93. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  94. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  95. package/lib/package.json +1 -0
  96. package/lib/plugin.js +119 -0
  97. package/lib/plugin.js.map +7 -0
  98. package/lib/storybook/avalanche-panel-model.js +79 -0
  99. package/lib/storybook/brand.jsx +6 -0
  100. package/lib/storybook/connection-fallback.jsx +47 -0
  101. package/lib/storybook/constants.js +15 -0
  102. package/lib/storybook/context-card.jsx +171 -0
  103. package/lib/storybook/manager-owner.js +42 -0
  104. package/lib/storybook/manager.jsx +425 -0
  105. package/lib/storybook/preset.js +7 -0
  106. package/lib/storybook/preview.js +298 -0
  107. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  108. package/lib/storybook/probe-intent.js +23 -0
  109. package/lib/storybook/setup.js +104 -0
  110. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  111. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  112. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  113. package/lib/types/avalanche/detector.d.ts +183 -0
  114. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  115. package/lib/types/avalanche/internals.d.ts +189 -0
  116. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  117. package/lib/types/avalanche/reporter.d.ts +610 -0
  118. package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
  119. package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
  120. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  121. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  122. package/lib/types/debug-render/core.d.ts +78 -0
  123. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  124. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  125. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  126. package/lib/types/package/avalanche.d.ts +6 -0
  127. package/lib/types/package/browser.d.ts +10 -0
  128. package/lib/types/package/plugin.d.ts +15 -0
  129. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  130. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  131. package/lib/types/plugins/index.d.ts +49 -0
  132. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  133. package/package.json +106 -4
@@ -0,0 +1,198 @@
1
+ // Drive the React hook-update tracker on the connected page.
2
+ //
3
+ // The react-dom patch only ever calls `metrics.onHookUpdate(fiber, kind, slot)` when
4
+ // a memo/callback/effect RE-COMPUTES because its deps actually changed (update path,
5
+ // post dep-equality check). It stores nothing itself. This snippet OWNS that callback
6
+ // — the one thing Chippy installs (a consumer hook, not a whole detector) — records
7
+ // per-(componentType, slot) update counts, and reports DB-safe histograms + dev-only
8
+ // offenders. Faithful port of react-patch `src/metrics/hookTracker.ts`.
9
+ //
10
+ // Hook kinds are numeric on the hot path: 1 = memo, 2 = callback, 3 = effect.
11
+ //
12
+ // State persists across eval-page calls on `window.__CHIPPY_HOOK_TRACKER` because
13
+ // install / interact / read / uninstall are separate invocations.
14
+ //
15
+ // Args:
16
+ // --action <name> REQUIRED. One of:
17
+ // install seed metrics if absent, take ownership of onHookUpdate, mode='counts'
18
+ // mode set --mode off|counts|debug
19
+ // status report installed/mode/entryCount/onHookUpdate ownership
20
+ // counts dump source-of-truth: per componentType -> {counts[],type[]} (compact)
21
+ // histogram DB-safe: per-kind counts of slot identities reaching each --buckets bucket
22
+ // offenders dev-only: hook slots with count > --threshold, carrying component name
23
+ // reset clear accumulated counts (stay installed)
24
+ // uninstall restore onHookUpdate (only if we still own it), keep last counts
25
+ // --mode <m> for action=mode: off | counts | debug
26
+ // --buckets <csv> for action=histogram (default "3,6,11"). Positive finite numbers only.
27
+ // --threshold <N> for action=offenders (default 10). Reports slots with count > N.
28
+ // --top <N> for action=offenders: cap rows (default 20).
29
+ //
30
+ // NOTE: install requires a metrics object. On hosts that wipe __ATL_REACT_METRICS
31
+ // to undefined (e.g. after hydration) this snippet SEEDS a minimal one so the tracker
32
+ // can own the slot — mirroring how the tracker is meant to be installed independently.
33
+ // It never overwrites an existing host object; it only adds the onHookUpdate slot.
34
+
35
+ const MEMO = 1, CALLBACK = 2, EFFECT = 3;
36
+ const KIND_NAME = { 1: 'memo', 2: 'callback', 3: 'effect' };
37
+
38
+ function getDisplayName(t) {
39
+ if (typeof t === 'function') return t.displayName || t.name || 'anonymous';
40
+ return typeof t === 'string' ? t : 'unknown';
41
+ }
42
+
43
+ // ── persistent tracker singleton on the page ────────────────────────────────
44
+ function getTracker() {
45
+ let T = globalThis.__CHIPPY_HOOK_TRACKER;
46
+ if (T && T.__v === 1) return T;
47
+
48
+ const state = {
49
+ __v: 1,
50
+ mode: 'off',
51
+ installed: false,
52
+ countsByType: new Map(), // componentType -> { counts:number[], type:number[] }
53
+ lastFiberByType: new Map(), // componentType -> fiber (debug mode only)
54
+ record: null,
55
+ };
56
+
57
+ function ctype(fiber) {
58
+ if (fiber == null || typeof fiber !== 'object') return 'unknown';
59
+ return fiber.elementType || fiber.type || 'unknown';
60
+ }
61
+
62
+ state.record = function record(fiber, kind, slot) {
63
+ if (state.mode === 'off') return;
64
+ const t = ctype(fiber);
65
+ let e = state.countsByType.get(t);
66
+ if (e == null) { e = { counts: [], type: [] }; state.countsByType.set(t, e); }
67
+ e.counts[slot] = (e.counts[slot] || 0) + 1;
68
+ e.type[slot] = kind;
69
+ if (state.mode === 'debug') state.lastFiberByType.set(t, fiber);
70
+ };
71
+
72
+ globalThis.__CHIPPY_HOOK_TRACKER = state;
73
+ return state;
74
+ }
75
+
76
+ function ensureMetrics() {
77
+ let m = globalThis.__ATL_REACT_METRICS;
78
+ if (m != null && typeof m === 'object') return { metrics: m, seeded: false };
79
+ // Host absent/wiped — seed a minimal object so the tracker can own onHookUpdate.
80
+ m = { setStateCount: 0 };
81
+ globalThis.__ATL_REACT_METRICS = m;
82
+ return { metrics: m, seeded: true };
83
+ }
84
+
85
+ const T = getTracker();
86
+ const action = String(ctx.args.action || '').trim();
87
+
88
+ if (action === 'install') {
89
+ const { metrics, seeded } = ensureMetrics();
90
+ // TODO: Preserve the displaced callback and owning metrics object on
91
+ // first install, then restore that callback only when this record function
92
+ // still owns the same slot. The current uninstall path calls this a restore
93
+ // but clears the slot to undefined, permanently dropping an earlier consumer.
94
+ metrics.onHookUpdate = T.record;
95
+ T.installed = true;
96
+ if (T.mode === 'off') T.mode = 'counts';
97
+ ctx.log.push('installed onHookUpdate; metrics ' + (seeded ? 'SEEDED' : 'existing') + '; mode=' + T.mode);
98
+ return { ok: true, action, installed: true, mode: T.mode, metricsSeeded: seeded, owns: metrics.onHookUpdate === T.record };
99
+ }
100
+
101
+ if (action === 'mode') {
102
+ const next = String(ctx.args.mode || '').trim();
103
+ if (!['off', 'counts', 'debug'].includes(next)) return { ok: false, error: "--mode must be off|counts|debug" };
104
+ T.mode = next;
105
+ return { ok: true, action, mode: T.mode };
106
+ }
107
+
108
+ if (action === 'status') {
109
+ const m = globalThis.__ATL_REACT_METRICS;
110
+ return {
111
+ ok: true, action,
112
+ installed: T.installed,
113
+ mode: T.mode,
114
+ owns: !!(m && m.onHookUpdate === T.record),
115
+ onHookUpdateType: m ? typeof m.onHookUpdate : 'no-metrics',
116
+ entryCount: T.countsByType.size,
117
+ setStateCount: m ? m.setStateCount : null,
118
+ };
119
+ }
120
+
121
+ if (action === 'reset') {
122
+ T.countsByType = new Map();
123
+ T.lastFiberByType = new Map();
124
+ return { ok: true, action, cleared: true, mode: T.mode, installed: T.installed };
125
+ }
126
+
127
+ if (action === 'uninstall') {
128
+ const m = globalThis.__ATL_REACT_METRICS;
129
+ let restored = false;
130
+ if (m != null && m.onHookUpdate === T.record) { m.onHookUpdate = undefined; restored = true; }
131
+ T.installed = false;
132
+ ctx.log.push('uninstalled; restored=' + restored);
133
+ return { ok: true, action, restored, entryCountRetained: T.countsByType.size };
134
+ }
135
+
136
+ if (action === 'counts') {
137
+ // Source of truth, compact. Aggregated by component TYPE (instances merge).
138
+ const rows = [];
139
+ for (const [t, e] of T.countsByType.entries()) {
140
+ let total = 0; const slots = [];
141
+ for (let s = 0; s < e.counts.length; s++) {
142
+ const c = e.counts[s] || 0;
143
+ if (!c) continue;
144
+ total += c;
145
+ slots.push({ slot: s, count: c, kind: KIND_NAME[e.type[s]] || e.type[s] });
146
+ }
147
+ rows.push({ type: getDisplayName(t), totalUpdates: total, activeSlots: slots.length, slots });
148
+ }
149
+ rows.sort((a, b) => b.totalUpdates - a.totalUpdates);
150
+ return { ok: true, action, mode: T.mode, entryCount: rows.length, types: rows };
151
+ }
152
+
153
+ if (action === 'histogram') {
154
+ const raw = ctx.args.buckets ? String(ctx.args.buckets) : '3,6,11';
155
+ const buckets = raw.split(',').map((s) => Number(s.trim()))
156
+ .filter((n) => Number.isFinite(n) && n > 0).sort((a, b) => a - b);
157
+ if (buckets.length === 0) return { ok: false, error: 'histogram requires at least one positive finite bucket' };
158
+ const out = { buckets: buckets.slice(), memo: buckets.map(() => 0), callback: buckets.map(() => 0), effect: buckets.map(() => 0) };
159
+ const arrFor = (k) => (k === MEMO ? out.memo : k === CALLBACK ? out.callback : out.effect);
160
+ for (const e of T.countsByType.values()) {
161
+ for (let s = 0; s < e.counts.length; s++) {
162
+ const c = e.counts[s] || 0;
163
+ if (!c) continue;
164
+ const k = e.type[s];
165
+ if (k !== MEMO && k !== CALLBACK && k !== EFFECT) continue;
166
+ const target = arrFor(k);
167
+ for (let b = 0; b < buckets.length; b++) if (c >= buckets[b]) target[b] += 1;
168
+ }
169
+ }
170
+ return { ok: true, action, histogram: out };
171
+ }
172
+
173
+ if (action === 'offenders') {
174
+ const threshold = Number.isFinite(Number(ctx.args.threshold)) ? Number(ctx.args.threshold) : 10;
175
+ const top = Number(ctx.args.top) || 20;
176
+ const offenders = [];
177
+ for (const [t, e] of T.countsByType.entries()) {
178
+ for (let s = 0; s < e.counts.length; s++) {
179
+ const c = e.counts[s] || 0;
180
+ if (c <= threshold) continue;
181
+ const k = e.type[s];
182
+ if (k !== MEMO && k !== CALLBACK && k !== EFFECT) continue;
183
+ const fiber = T.lastFiberByType.get(t);
184
+ offenders.push({
185
+ component: getDisplayName(t),
186
+ source: fiber && fiber._debugSource ? { fileName: fiber._debugSource.fileName, lineNumber: fiber._debugSource.lineNumber } : null,
187
+ hookId: s,
188
+ type: KIND_NAME[k],
189
+ count: c,
190
+ });
191
+ }
192
+ }
193
+ offenders.sort((a, b) => b.count - a.count);
194
+ if (T.mode !== 'debug') ctx.log.push('mode is "' + T.mode + '": source locations require --mode debug before reproducing.');
195
+ return { ok: true, action, threshold, mode: T.mode, offenders: offenders.slice(0, top) };
196
+ }
197
+
198
+ return { ok: false, error: 'unknown --action "' + action + '". Use install|mode|status|counts|histogram|offenders|reset|uninstall' };
@@ -0,0 +1,101 @@
1
+ // Stability render nudge — trigger a controlled re-render on a single fiber.
2
+ //
3
+ // PROCEDURE layer. The CAPABILITY (forcing a re-render via the React DevTools
4
+ // renderer's scheduleUpdate) lives in the bundle:
5
+ // src/chippy-bridge/fiber-schedule-update.ts → window.__AVALANCHE__.bridge.evalFiberScheduleUpdate
6
+ //
7
+ // This snippet composes the stability-render procedure on top of that capability:
8
+ // it resolves a probe/testId to a component fiber (DOM context the bridge does
9
+ // not have), optionally climbs the owner chain, then asks the capability to
10
+ // schedule exactly one re-render. Mutates nothing.
11
+ //
12
+ // Because the mechanism lives in the bundle, this procedure can evolve here with
13
+ // zero extension/store publish. See references/stability-render.md.
14
+ //
15
+ // Args (one of --probeId or --testId is required):
16
+ // --probeId <id> probe id ("1" | "2" | "3")
17
+ // --testId <string> data-testid of the target element
18
+ // --ownerIndex <N> optional, walk N steps up owner chain (default 0)
19
+ // --label <string> optional, echoed into ctx.log
20
+ // --dryRun <true|false> resolve + report fiber, do not schedule
21
+ //
22
+ // Returns: { ok, fiberName, source, ownerIndex, label?, reason? }
23
+
24
+ const probeId = ctx.args.probeId == null ? null : String(ctx.args.probeId);
25
+ const testId = ctx.args.testId == null ? null : String(ctx.args.testId);
26
+ const ownerIndex = Number.isFinite(Number(ctx.args.ownerIndex)) ? (Number(ctx.args.ownerIndex) | 0) : 0;
27
+ const label = ctx.args.label != null ? String(ctx.args.label) : null;
28
+ const dryRun = ctx.args.dryRun === true || ctx.args.dryRun === 'true';
29
+
30
+ if (!probeId && !testId) return { ok: false, reason: 'missing-target (need --probeId or --testId)' };
31
+ if (label) ctx.log.push('label: ' + label);
32
+
33
+ // 1. Resolve target → DOM node → composite fiber.
34
+ function fiberFromNode(node) {
35
+ if (!node) return null;
36
+ const keys = Object.keys(node).filter((k) => k.startsWith('__reactFiber$'));
37
+ let f = keys.length ? node[keys[0]] : null;
38
+ while (f && (typeof f.type === 'string' || f.type == null)) f = f.return;
39
+ return f;
40
+ }
41
+
42
+ let fiber = null;
43
+ if (testId) {
44
+ const node = document.querySelector('[data-testid="' + testId.replace(/"/g, '\\"') + '"]');
45
+ if (!node) return { ok: false, reason: 'testid-not-found: ' + testId, label };
46
+ ctx.log.push('resolved by testId: ' + testId);
47
+ fiber = fiberFromNode(node);
48
+ if (!fiber) return { ok: false, reason: 'no-composite-fiber-for-testid', label };
49
+ } else {
50
+ const resolveAll = globalThis.__resolveProbes || globalThis.__resolveProbesCompact;
51
+ if (typeof resolveAll !== 'function') return { ok: false, reason: 'no-resolver', label };
52
+ const resolved = resolveAll(probeId);
53
+ const entry = Array.isArray(resolved) ? resolved[0] : resolved && resolved[probeId];
54
+ if (entry && entry.__fiber) {
55
+ fiber = entry.__fiber;
56
+ } else {
57
+ const lab = globalThis.__layoutLab;
58
+ const probe = lab && lab.getMarkerProbe ? lab.getMarkerProbe(probeId) : null;
59
+ const node = probe && probe.element;
60
+ if (!node) return { ok: false, reason: 'no-fiber', label };
61
+ fiber = fiberFromNode(node);
62
+ if (!fiber) return { ok: false, reason: 'no-composite-fiber', label };
63
+ }
64
+ }
65
+
66
+ if (dryRun) {
67
+ // Climb here only for reporting; the capability does its own walk on the real run.
68
+ let preview = fiber;
69
+ let walked = 0;
70
+ while (walked < ownerIndex && preview) { preview = preview._debugOwner || null; walked++; }
71
+ if (!preview) return { ok: false, reason: 'owner-out-of-range', label };
72
+ const name =
73
+ (preview.type && (preview.type.displayName || preview.type.name)) ||
74
+ (preview.elementType && (preview.elementType.displayName || preview.elementType.name)) ||
75
+ '<anonymous>';
76
+ const src = preview._debugSource
77
+ ? { fileName: preview._debugSource.fileName, lineNumber: preview._debugSource.lineNumber }
78
+ : null;
79
+ ctx.log.push('dryRun target: ' + name + (src ? ' @ ' + src.fileName + ':' + src.lineNumber : ''));
80
+ return { ok: true, dryRun: true, fiberName: name, source: src, ownerIndex, label };
81
+ }
82
+
83
+ // 2. Delegate the actual re-render to the bundle capability. We already hold the
84
+ // fiber, so we hand the capability deps that resolve straight to it.
85
+ const av = globalThis.__AVALANCHE__;
86
+ const bridge = av && av.bridge;
87
+ if (!bridge || typeof bridge.evalFiberScheduleUpdate !== 'function') {
88
+ return { ok: false, reason: 'bridge-capability-missing (avalanche.iife.js not loaded)', label };
89
+ }
90
+
91
+ const FIXED_ID = '__nudge_target__';
92
+ const result = bridge.evalFiberScheduleUpdate(FIXED_ID, ownerIndex, {
93
+ normalizeRememberedFiberId: (id) => (id === FIXED_ID ? FIXED_ID : null),
94
+ getRememberedFiber: (id) => (id === FIXED_ID ? fiber : null),
95
+ });
96
+
97
+ if (result && result.ok) {
98
+ ctx.log.push('scheduled: ' + result.fiberName + (result.source ? ' @ ' + result.source.fileName + ':' + result.source.lineNumber : ''));
99
+ return { ok: true, fiberName: result.fiberName, source: result.source, ownerIndex: result.ownerIndex, label };
100
+ }
101
+ return { ok: false, reason: (result && result.reason) || 'schedule-failed', error: result && result.error, fiberName: result && result.fiberName, label };
@@ -0,0 +1 @@
1
+ {"type":"module","sideEffects":["./bootstrap.js","./storybook/**","./extension/**"]}
package/lib/plugin.js ADDED
@@ -0,0 +1,119 @@
1
+ // src/chippy-bridge/source-map.ts
2
+ var DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ var VALUE_OF = new Int8Array(128).fill(-1);
4
+ for (let index = 0; index < DIGITS.length; index += 1) {
5
+ VALUE_OF[DIGITS.charCodeAt(index)] = index;
6
+ }
7
+
8
+ // src/chippy-bridge/fiber-source.ts
9
+ var V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
10
+ var AT_FRAME = /^\s*(?:(.*?)@)(.+?):(\d+):(\d+)$/;
11
+ var MAX_CALL_SITE_FRAMES = 4;
12
+ function parseStackFrames(stack) {
13
+ const frames = [];
14
+ for (const raw of stack.split("\n")) {
15
+ const line = raw.trimEnd();
16
+ if (line === "" || line.includes("(eval at ")) continue;
17
+ const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);
18
+ if (!match) continue;
19
+ const [, name, url = "", lineDigits = "", columnDigits = ""] = match;
20
+ if (url === "" || url.startsWith("data:")) continue;
21
+ const at = Number(lineDigits);
22
+ const column = Number(columnDigits);
23
+ if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;
24
+ frames.push({
25
+ url,
26
+ line: at,
27
+ column,
28
+ ...name !== void 0 && name !== "" ? { function: name } : {}
29
+ });
30
+ }
31
+ return frames;
32
+ }
33
+ function isVendorPath(path) {
34
+ return path.includes("node_modules") || path.includes("/.vite/deps/") || path.includes("/.yarn/");
35
+ }
36
+ function rawLocation(frame) {
37
+ return { fileName: frame.url, lineNumber: frame.line, columnNumber: frame.column };
38
+ }
39
+ function locationFromOriginal(original) {
40
+ return { fileName: original.source, lineNumber: original.line, columnNumber: original.column };
41
+ }
42
+ function writerLocationOf(frames, originalFor) {
43
+ for (const frame of frames) {
44
+ if (isVendorPath(frame.url)) continue;
45
+ const original = originalFor(frame);
46
+ if (original === null) return rawLocation(frame);
47
+ if (isVendorPath(original.source)) continue;
48
+ return locationFromOriginal(original);
49
+ }
50
+ return null;
51
+ }
52
+ function debugSourceOf(value) {
53
+ if (!value || typeof value !== "object") return null;
54
+ const source = value;
55
+ if (typeof source.fileName !== "string" || typeof source.lineNumber !== "number") return null;
56
+ return {
57
+ fileName: source.fileName,
58
+ lineNumber: source.lineNumber,
59
+ columnNumber: typeof source.columnNumber === "number" ? source.columnNumber : null
60
+ };
61
+ }
62
+ function recordedSource(fiber) {
63
+ const direct = debugSourceOf(fiber._debugSource);
64
+ if (direct) return direct;
65
+ const type = fiber.type;
66
+ return type && (typeof type === "object" || typeof type === "function") ? debugSourceOf(type._debugSource) : null;
67
+ }
68
+ function getFiberStackFrames(fiber) {
69
+ if (!fiber || !fiber._debugStack || typeof fiber._debugStack !== "object") return [];
70
+ const stack = fiber._debugStack.stack;
71
+ if (typeof stack !== "string" || stack === "") return [];
72
+ const candidates = [];
73
+ for (const frame of parseStackFrames(stack)) {
74
+ if (isVendorPath(frame.url)) continue;
75
+ candidates.push(frame);
76
+ if (candidates.length >= MAX_CALL_SITE_FRAMES) break;
77
+ }
78
+ return candidates;
79
+ }
80
+ function getFiberSourceCandidate(fiber) {
81
+ if (!fiber) return null;
82
+ const recorded = recordedSource(fiber);
83
+ if (recorded) return recorded;
84
+ return writerLocationOf(getFiberStackFrames(fiber), () => null);
85
+ }
86
+
87
+ // src/layout-lab/fiber-walk.ts
88
+ function getDisplayName(type) {
89
+ if (!type) return null;
90
+ if (typeof type === "string") return type;
91
+ const obj = type;
92
+ return obj.displayName || obj.name || null;
93
+ }
94
+
95
+ // src/avalanche/internals.tsx
96
+ var COMPONENT_TAGS = /* @__PURE__ */ new Set([0, 1, 2, 11, 14, 15]);
97
+ function isComponentFiber(tag) {
98
+ return COMPONENT_TAGS.has(tag);
99
+ }
100
+ function getComponentName(fiber) {
101
+ const type = fiber.type;
102
+ const elementType = fiber.elementType;
103
+ const named = type?.displayName || type?.name || elementType?.displayName || elementType?.name;
104
+ if (named) return named;
105
+ const source = getFiberSourceCandidate(fiber);
106
+ return source && `<${source.fileName}:${source.lineNumber}>` || "Anonymous";
107
+ }
108
+
109
+ // src/package/plugin.ts
110
+ function definePlugin(plugin) {
111
+ return plugin;
112
+ }
113
+ export {
114
+ definePlugin,
115
+ getComponentName,
116
+ getDisplayName,
117
+ isComponentFiber
118
+ };
119
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/chippy-bridge/source-map.ts", "../src/chippy-bridge/fiber-source.ts", "../src/layout-lab/fiber-walk.ts", "../src/avalanche/internals.tsx", "../src/package/plugin.ts"],
4
+ "sourcesContent": ["/**\n * Small, dependency-free Source Map v3 reader.\n *\n * Chippy only needs the generated-position lookup used by React 19's\n * `_debugStack` source path, so it deliberately omits names and source contents.\n */\n\nconst DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst VALUE_OF = new Int8Array(128).fill(-1);\nfor (let index = 0; index < DIGITS.length; index += 1) {\n VALUE_OF[DIGITS.charCodeAt(index)] = index;\n}\n\ntype Segment = {\n generatedColumn: number;\n sourceIndex: number | null;\n originalLine: number | null;\n originalColumn: number | null;\n};\n\nexport type SourceMap = {\n sources: readonly string[];\n lines: readonly (readonly Segment[])[];\n offset: { line: number; column: number };\n};\n\nexport type OriginalPosition = {\n source: string;\n line: number;\n column: number;\n};\n\ntype RawMap = {\n version?: unknown;\n sources?: unknown;\n sourceRoot?: unknown;\n mappings?: unknown;\n sections?: unknown;\n};\n\nfunction decodeVlq(field: string): number[] {\n const values: number[] = [];\n let value = 0;\n let shift = 0;\n\n for (let index = 0; index < field.length; index += 1) {\n const code = field.charCodeAt(index);\n const digit = code < 128 ? VALUE_OF[code]! : -1;\n if (digit < 0) return values;\n\n const more = (digit & 32) !== 0;\n value += (digit & 31) << shift;\n if (more) {\n shift += 5;\n continue;\n }\n\n const negative = (value & 1) === 1;\n const magnitude = value >>> 1;\n values.push(negative ? (magnitude === 0 ? -0x80000000 : -magnitude) : magnitude);\n value = 0;\n shift = 0;\n }\n\n return values;\n}\n\nfunction decodeMappings(mappings: string, sourceCount: number): Segment[][] {\n const lines: Segment[][] = [];\n let sourceIndex = 0;\n let originalLine = 0;\n let originalColumn = 0;\n\n for (const encodedLine of mappings.split(';')) {\n const segments: Segment[] = [];\n let generatedColumn = 0;\n\n for (const field of encodedLine === '' ? [] : encodedLine.split(',')) {\n const values = decodeVlq(field);\n if (values.length < 4) {\n if (values.length > 0) {\n generatedColumn += values[0]!;\n segments.push({ generatedColumn, sourceIndex: null, originalLine: null, originalColumn: null });\n }\n continue;\n }\n\n generatedColumn += values[0]!;\n sourceIndex += values[1]!;\n originalLine += values[2]!;\n originalColumn += values[3]!;\n if (sourceIndex >= 0 && sourceIndex < sourceCount) {\n segments.push({ generatedColumn, sourceIndex, originalLine, originalColumn });\n }\n }\n\n segments.sort((left, right) => left.generatedColumn - right.generatedColumn);\n lines.push(segments);\n }\n\n return lines;\n}\n\nfunction resolveSources(raw: RawMap): string[] | null {\n if (!Array.isArray(raw.sources)) return null;\n const root = typeof raw.sourceRoot === 'string' ? raw.sourceRoot : '';\n\n return raw.sources.map((source) => {\n if (typeof source !== 'string') return '';\n if (root === '' || source === '' || /^[a-z][a-z0-9+.-]*:/i.test(source)) return source;\n if (root.endsWith('/') && source.startsWith('/')) return `${root}${source.slice(1)}`;\n if (root.endsWith('/') || source.startsWith('/')) return `${root}${source}`;\n return `${root}/${source}`;\n });\n}\n\nexport function parseSourceMap(text: string): SourceMap[] {\n let raw: RawMap;\n try {\n raw = JSON.parse(text) as RawMap;\n } catch {\n return [];\n }\n\n if (raw === null || typeof raw !== 'object') return [];\n if (raw.version !== 3) return [];\n\n if (Array.isArray(raw.sections)) {\n return raw.sections.flatMap((section) => {\n if (section === null || typeof section !== 'object') return [];\n const { offset, map } = section as { offset?: unknown; map?: unknown };\n if (offset === null || typeof offset !== 'object' || map === null || typeof map !== 'object') return [];\n const { line, column } = offset as { line?: unknown; column?: unknown };\n if (typeof line !== 'number' || typeof column !== 'number') return [];\n return parseSourceMap(JSON.stringify(map)).map((parsed) => ({ ...parsed, offset: { line, column } }));\n });\n }\n\n const sources = resolveSources(raw);\n if (sources === null || typeof raw.mappings !== 'string') return [];\n return [{ sources, lines: decodeMappings(raw.mappings, sources.length), offset: { line: 0, column: 0 } }];\n}\n\nfunction sectionsFor(maps: readonly SourceMap[], line: number, column: number): readonly SourceMap[] {\n if (\n maps.length === 0 ||\n (maps.length === 1 && maps[0]?.offset.line === 0 && maps[0]?.offset.column === 0)\n ) return maps;\n const zeroBasedLine = line - 1;\n const candidates = maps.filter(\n (map) =>\n map.offset.line < zeroBasedLine ||\n (map.offset.line === zeroBasedLine && map.offset.column < column),\n );\n return candidates.length === 0 ? [] : [candidates[candidates.length - 1]!];\n}\n\nfunction segmentAt(segments: readonly Segment[], column: number): Segment | undefined {\n let low = 0;\n let high = segments.length - 1;\n let found: Segment | undefined;\n\n while (low <= high) {\n const middle = (low + high) >> 1;\n const segment = segments[middle]!;\n if (segment.generatedColumn <= column) {\n found = segment;\n low = middle + 1;\n } else {\n high = middle - 1;\n }\n }\n\n return found;\n}\n\nexport function originalPositionFor(\n maps: readonly SourceMap[],\n line: number,\n column: number,\n): OriginalPosition | null {\n for (const map of sectionsFor(maps, line, column)) {\n const localLine = line - map.offset.line;\n const localColumn = localLine === 1 ? column - map.offset.column : column;\n const segments = map.lines[localLine - 1];\n if (segments === undefined || segments.length === 0) continue;\n\n const found = segmentAt(segments, localColumn - 1);\n if (found === undefined) continue;\n if (\n found.sourceIndex === null ||\n found.originalLine === null ||\n found.originalColumn === null\n ) return null;\n const source = map.sources[found.sourceIndex];\n if (source === undefined || source === '') continue;\n return { source, line: found.originalLine + 1, column: found.originalColumn + 1 };\n }\n\n return null;\n}\n\nexport function sourceMappingUrlOf(code: string): string | null {\n const found = [...code.matchAll(/^[/*]{2,}[#@]\\s*sourceMappingURL=(\\S+)\\s*(?:\\*\\/)?$/gm)];\n return found.at(-1)?.[1] ?? null;\n}\n\nexport function inlineSourceMapOf(url: string): string | null {\n const match = /^data:application\\/json[^,]*,(.*)$/s.exec(url);\n if (match === null) return null;\n const payload = match[1]!;\n\n if (!/;base64/i.test(url)) {\n try {\n return decodeURIComponent(payload);\n } catch {\n return null;\n }\n }\n\n try {\n const decode = (globalThis as { atob?: (input: string) => string }).atob;\n if (decode) {\n const binary = decode(payload);\n const Decoder = (globalThis as {\n TextDecoder?: new () => { decode(bytes: Uint8Array): string };\n }).TextDecoder;\n return Decoder\n ? new Decoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)))\n : binary;\n }\n const buffer = (globalThis as {\n Buffer?: { from(value: string, encoding: string): { toString(encoding: string): string } };\n }).Buffer;\n return buffer?.from(payload, 'base64').toString('utf8') ?? null;\n } catch {\n return null;\n }\n}\n", "import {\n inlineSourceMapOf,\n originalPositionFor,\n parseSourceMap,\n sourceMappingUrlOf,\n type OriginalPosition,\n type SourceMap,\n} from './source-map';\nimport { baseName, pageModuleUrls, recordedLiteralPosition } from './recorded-source';\n\nexport type FiberSourceLocation = {\n fileName: string;\n lineNumber: number;\n columnNumber: number | null;\n};\n\nexport type StackFrame = {\n url: string;\n line: number;\n column: number;\n function?: string;\n};\n\nexport type SourceFiber = {\n type?: unknown;\n _debugSource?: unknown;\n _debugStack?: unknown;\n};\n\nexport type FetchModule = (url: string) => Promise<string | null>;\n\n/** Module URLs that may serve a file named by React <=18 `_debugSource`. */\nexport type ModuleUrlsFor = (fileName: string) => readonly string[];\n\nexport type CallSiteResolver = {\n locate(frames: readonly StackFrame[]): Promise<FiberSourceLocation | null>;\n /** Map a recorded `_debugSource` position through its served module. */\n locateRecorded(source: FiberSourceLocation): Promise<FiberSourceLocation>;\n readonly stats: { modules: number; sites: number; located: number };\n};\n\ntype ResolvedMaps = {\n code: string;\n maps: readonly SourceMap[];\n sourceBaseUrl: string;\n};\n\nconst V8_FRAME = /^\\s*at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\nconst AT_FRAME = /^\\s*(?:(.*?)@)(.+?):(\\d+):(\\d+)$/;\nconst MAX_CALL_SITE_FRAMES = 4;\n\nconst resolvedFiberSources = new WeakMap<object, FiberSourceLocation | null>();\nconst pendingFiberSources = new WeakMap<object, Promise<FiberSourceLocation | null>>();\nlet defaultResolver: CallSiteResolver | null = null;\n\nexport function parseStackFrames(stack: string): StackFrame[] {\n const frames: StackFrame[] = [];\n\n for (const raw of stack.split('\\n')) {\n const line = raw.trimEnd();\n if (line === '' || line.includes('(eval at ')) continue;\n const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);\n if (!match) continue;\n\n const [, name, url = '', lineDigits = '', columnDigits = ''] = match;\n if (url === '' || url.startsWith('data:')) continue;\n const at = Number(lineDigits);\n const column = Number(columnDigits);\n if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;\n frames.push({\n url,\n line: at,\n column,\n ...(name !== undefined && name !== '' ? { function: name } : {}),\n });\n }\n\n return frames;\n}\n\nexport function isVendorPath(path: string): boolean {\n return (\n path.includes('node_modules') ||\n path.includes('/.vite/deps/') ||\n path.includes('/.yarn/')\n );\n}\n\nfunction rawLocation(frame: StackFrame): FiberSourceLocation {\n return { fileName: frame.url, lineNumber: frame.line, columnNumber: frame.column };\n}\n\nfunction locationFromOriginal(original: OriginalPosition): FiberSourceLocation {\n return { fileName: original.source, lineNumber: original.line, columnNumber: original.column };\n}\n\nexport function writerLocationOf(\n frames: readonly StackFrame[],\n originalFor: (frame: StackFrame) => OriginalPosition | null,\n): FiberSourceLocation | null {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n const original = originalFor(frame);\n if (original === null) return rawLocation(frame);\n if (isVendorPath(original.source)) continue;\n return locationFromOriginal(original);\n }\n return null;\n}\n\nfunction parseUrl(url: string, base?: string): URL | null {\n try {\n return new URL(url, base);\n } catch {\n return null;\n }\n}\n\nfunction sourcePath(source: string, moduleUrl: string): string {\n if (/^\\/(?:Users|home|private|Volumes)\\//.test(source)) return source;\n const direct = parseUrl(source);\n if (direct?.protocol === 'file:') return decodeURIComponent(direct.pathname);\n const resolved = parseUrl(source, moduleUrl)?.href ?? source;\n const parsed = parseUrl(resolved);\n return parsed ? parsed.pathname.replace(/^\\/+/, '') : resolved;\n}\n\nexport function createCallSiteResolver(\n fetchModule: FetchModule,\n moduleUrlsFor: ModuleUrlsFor = () => [],\n): CallSiteResolver {\n const maps = new Map<string, Promise<ResolvedMaps | null>>();\n const sites = new Map<string, Promise<FiberSourceLocation | null>>();\n const recordedSites = new Map<string, Promise<FiberSourceLocation>>();\n const stats = { modules: 0, sites: 0, located: 0 };\n\n async function mapsFor(url: string): Promise<ResolvedMaps | null> {\n const cached = maps.get(url);\n if (cached) return cached;\n\n const pending = (async () => {\n stats.modules += 1;\n const code = await fetchModule(url);\n if (code === null) return null;\n const annotation = sourceMappingUrlOf(code);\n if (annotation === null) return null;\n const inline = inlineSourceMapOf(annotation);\n if (inline !== null) return { code, maps: parseSourceMap(inline), sourceBaseUrl: url };\n const mapUrl = parseUrl(annotation, url)?.href ?? null;\n const text = mapUrl === null ? null : await fetchModule(mapUrl);\n return text === null || mapUrl === null\n ? null\n : { code, maps: parseSourceMap(text), sourceBaseUrl: mapUrl };\n })();\n\n maps.set(url, pending);\n return pending;\n }\n\n async function locateFrame(frame: StackFrame): Promise<FiberSourceLocation | null> {\n const parsed = await mapsFor(frame.url);\n if (!parsed || parsed.maps.length === 0) return null;\n const original = originalPositionFor(parsed.maps, frame.line, frame.column);\n if (!original) return null;\n return {\n fileName: sourcePath(original.source, parsed.sourceBaseUrl),\n lineNumber: original.line,\n columnNumber: original.column,\n };\n }\n\n async function locate(frames: readonly StackFrame[]): Promise<FiberSourceLocation | null> {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n const key = `${frame.url}:${frame.line}:${frame.column}`;\n let answer = sites.get(key);\n if (!answer) {\n stats.sites += 1;\n answer = locateFrame(frame);\n sites.set(key, answer);\n }\n\n const located = await answer;\n const chosen = located === null\n ? writerLocationOf([frame], () => null)\n : isVendorPath(located.fileName) ? null : located;\n if (chosen) {\n stats.located += 1;\n return chosen;\n }\n }\n return null;\n }\n\n async function correctRecorded(source: FiberSourceLocation): Promise<FiberSourceLocation> {\n for (const url of moduleUrlsFor(source.fileName)) {\n const parsed = await mapsFor(url);\n if (!parsed || parsed.maps.length === 0) continue;\n const served = recordedLiteralPosition(parsed.code, source);\n if (served === null) continue;\n const original = originalPositionFor(parsed.maps, served.line, served.column);\n // The literal must map back into the recorded file, not a neighbor.\n if (!original || baseName(original.source) !== baseName(source.fileName)) continue;\n return { fileName: source.fileName, lineNumber: original.line, columnNumber: original.column };\n }\n return source;\n }\n\n function locateRecorded(source: FiberSourceLocation): Promise<FiberSourceLocation> {\n const key = `${source.fileName}:${source.lineNumber}:${source.columnNumber}`;\n let answer = recordedSites.get(key);\n if (!answer) {\n answer = correctRecorded(source).catch(() => source);\n recordedSites.set(key, answer);\n }\n return answer;\n }\n\n return { locate, locateRecorded, stats };\n}\n\nfunction debugSourceOf(value: unknown): FiberSourceLocation | null {\n if (!value || typeof value !== 'object') return null;\n const source = value as { fileName?: unknown; lineNumber?: unknown; columnNumber?: unknown };\n if (typeof source.fileName !== 'string' || typeof source.lineNumber !== 'number') return null;\n return {\n fileName: source.fileName,\n lineNumber: source.lineNumber,\n columnNumber: typeof source.columnNumber === 'number' ? source.columnNumber : null,\n };\n}\n\nfunction recordedSource(fiber: SourceFiber): FiberSourceLocation | null {\n const direct = debugSourceOf(fiber._debugSource);\n if (direct) return direct;\n const type = fiber.type;\n return type && (typeof type === 'object' || typeof type === 'function')\n ? debugSourceOf((type as { _debugSource?: unknown })._debugSource)\n : null;\n}\n\nexport function getFiberStackFrames(fiber: SourceFiber | null | undefined): StackFrame[] {\n if (!fiber || !fiber._debugStack || typeof fiber._debugStack !== 'object') return [];\n const stack = (fiber._debugStack as { stack?: unknown }).stack;\n if (typeof stack !== 'string' || stack === '') return [];\n const candidates: StackFrame[] = [];\n for (const frame of parseStackFrames(stack)) {\n if (isVendorPath(frame.url)) continue;\n candidates.push(frame);\n if (candidates.length >= MAX_CALL_SITE_FRAMES) break;\n }\n return candidates;\n}\n\n/** Stable synchronous source, before any async source-map upgrade. */\nexport function getFiberSourceCandidate(fiber: SourceFiber | null | undefined): FiberSourceLocation | null {\n if (!fiber) return null;\n const recorded = recordedSource(fiber);\n if (recorded) return recorded;\n return writerLocationOf(getFiberStackFrames(fiber), () => null);\n}\n\n/**\n * Synchronous source read used by fiber walks and snapshots: the resolved\n * source once `resolveFiberSource` has run for this fiber, else the candidate.\n */\nexport function getFiberSource(fiber: SourceFiber | null | undefined): FiberSourceLocation | null {\n if (!fiber) return null;\n const cached = resolvedFiberSources.get(fiber as object);\n if (cached !== undefined) return cached;\n return getFiberSourceCandidate(fiber);\n}\n\nasync function fetchModuleFromPage(url: string): Promise<string | null> {\n const fetchFn = (globalThis as { fetch?: typeof fetch }).fetch;\n if (!fetchFn) return null;\n const controller = typeof AbortController === 'function' ? new AbortController() : null;\n const timeout = controller ? setTimeout(() => controller.abort(), 2000) : null;\n try {\n const response = await fetchFn(url, {\n credentials: 'same-origin',\n signal: controller?.signal,\n });\n return response.ok ? await response.text() : null;\n } catch {\n return null;\n } finally {\n if (timeout !== null) clearTimeout(timeout);\n }\n}\n\nfunction pageResolver(): CallSiteResolver {\n if (!defaultResolver) defaultResolver = createCallSiteResolver(fetchModuleFromPage, pageModuleUrls);\n return defaultResolver;\n}\n\n/**\n * Resolve a fiber's authored source through the served module's inline or\n * external map: the React 19 `_debugStack` call site, or the React <=18\n * `_debugSource` literal when a transform shifted its recorded line.\n */\nexport function resolveFiberSource(\n fiber: SourceFiber | null | undefined,\n resolver: CallSiteResolver = pageResolver(),\n): Promise<FiberSourceLocation | null> {\n if (!fiber) return Promise.resolve(null);\n const cached = pendingFiberSources.get(fiber as object);\n if (cached) return cached;\n\n const recorded = recordedSource(fiber);\n if (recorded) {\n const pending = resolver.locateRecorded(recorded).then((located) => {\n resolvedFiberSources.set(fiber as object, located);\n return located;\n });\n pendingFiberSources.set(fiber as object, pending);\n return pending;\n }\n\n const frames = getFiberStackFrames(fiber);\n const fallback = writerLocationOf(frames, () => null);\n const pending = resolver.locate(frames)\n .catch(() => null)\n .then((source) => {\n const located = source ?? fallback;\n resolvedFiberSources.set(fiber as object, located);\n return located;\n });\n pendingFiberSources.set(fiber as object, pending);\n return pending;\n}\n", "/**\n * Pure fiber-walk helpers extracted from layout-lab.js.\n * No DOM writes, no side effects \u2014 read-only introspection of React fiber trees.\n */\n\nimport {\n getFiberSource as readFiberSource,\n resolveFiberSource as resolveSourceLocation,\n} from '../chippy-bridge/fiber-source';\n\n/**\n * Minimal duck-typed Fiber shape.\n * Only fields used by these helpers are typed; rest may exist but are not read.\n */\nexport type Fiber = {\n type?: unknown;\n elementType?: unknown;\n key?: string | null;\n memoizedState?: unknown;\n memoizedProps?: Record<string, unknown> | null;\n stateNode?: Element | null;\n return?: Fiber | null;\n child?: Fiber | null;\n sibling?: Fiber | null;\n _debugOwner?: Fiber | null;\n _debugSource?: SourceLocation | null;\n _debugStack?: unknown;\n};\n\nexport type SourceLocation = {\n fileName?: string | null;\n lineNumber?: number | null;\n columnNumber?: number | null;\n};\n\nexport type DescribedSource = {\n fileName: string | null;\n lineNumber: number | null;\n columnNumber: number | null;\n};\n\nexport type HookSummary = {\n hookCount: number;\n hookCountTruncated: boolean;\n hookCountMax: number;\n};\n\nexport type OwnerEntry = {\n name: string | null;\n key: string | null;\n props: string[];\n} & HookSummary & {\n source: DescribedSource | null;\n};\n\nexport type FiberTarget = {\n domTag: string | null;\n domText: string;\n fiberType: string | null;\n source: DescribedSource | null;\n};\n\nexport type ComponentDescription = {\n name: string | null;\n key: string | null;\n props: Record<string, unknown>;\n source: DescribedSource | null;\n stateNode: string | null;\n owners: OwnerEntry[];\n parents: OwnerEntry[];\n} & HookSummary;\n\n/**\n * Find the React fiber key in a DOM node's own properties.\n * Returns the property name (e.g. '__reactFiber$abc123') or null.\n */\nexport function getFiberKey(node: Node | Element | null | undefined): string | null {\n if (!node) return null;\n const keys = Object.getOwnPropertyNames(node as object);\n const key = keys.find((k) => k.startsWith('__reactFiber$'));\n return key || null;\n}\n\n/**\n * Get the React fiber attached to a DOM node, or null if not a React host node.\n */\nexport function getFiber(node: Node | Element | null | undefined): Fiber | null {\n if (!node) return null;\n const key = getFiberKey(node);\n // React stores the fiber as an expando property on the host node.\n return key ? ((node as unknown as Record<string, unknown>)[key] as Fiber | null) : null;\n}\n\n/**\n * Extract display name from a React type (class, function, or string).\n * Returns displayName > name > null for functions; 'string' for primitives.\n */\nexport function getDisplayName(type: unknown): string | null {\n if (!type) return null;\n if (typeof type === 'string') return type;\n const obj = type as { displayName?: unknown; name?: unknown };\n return (obj.displayName as string) || (obj.name as string) || null;\n}\n\n/**\n * True if fiber is a user-defined component (not a DOM string, not a hook-internal wrapper).\n */\nexport function isUserComponent(fiber: Fiber | null | undefined): boolean {\n if (!fiber) return false;\n const name = getDisplayName(fiber.type);\n if (!name || typeof fiber.type === 'string') return false;\n if (['hookified', 'unboundStoryFn', 'Anonymous'].includes(name)) return false;\n return true;\n}\n\n/**\n * Walk DOM parents to find the nearest fiber-attached element.\n */\nexport function getNearestFiber(domNode: Node | Element | null | undefined): Fiber | null {\n let current = domNode as Element | null;\n while (current) {\n const fiber = getFiber(current);\n if (fiber) return fiber;\n current = (current as Element).parentElement;\n }\n return null;\n}\n\n/**\n * Walk DOM parents, then walk fiber return chain, to find the nearest user component.\n */\nexport function getNearestComponent(domNode: Node | Element | null | undefined): Fiber | null {\n let fiber = getNearestFiber(domNode);\n while (fiber) {\n if (isUserComponent(fiber)) return fiber;\n fiber = fiber.return || null;\n }\n return null;\n}\n\n/**\n * Count hooks in fiber.memoizedState linked list; detect cycle.\n */\nexport function getHookSummary(fiber: Fiber | null | undefined): HookSummary {\n const MAX_HOOKS = 500;\n const seen = new Set<unknown>();\n let hookNode = fiber?.memoizedState || null;\n let hookCount = 0;\n let hookCountTruncated = false;\n\n while (hookNode) {\n if (seen.has(hookNode)) break;\n if (hookCount >= MAX_HOOKS) {\n hookCountTruncated = true;\n break;\n }\n seen.add(hookNode);\n hookCount += 1;\n hookNode = (hookNode as { next?: unknown }).next || null;\n }\n\n return { hookCount, hookCountTruncated, hookCountMax: MAX_HOOKS };\n}\n\n/**\n * Walk fiber._debugOwner chain; collect all user components with metadata.\n * Returns empty array if fiber or owner chain is null.\n */\nexport function getOwnerChain(fiber: Fiber | null | undefined): OwnerEntry[] {\n const chain: OwnerEntry[] = [];\n let current = fiber?._debugOwner || null;\n while (current) {\n if (isUserComponent(current)) {\n chain.push({\n name: getDisplayName(current.type),\n key: current.key || null,\n props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],\n ...getHookSummary(current),\n source: describeFiberSource(current),\n });\n }\n current = current._debugOwner || null;\n }\n return chain;\n}\n\n/**\n * Walk fiber._debugOwner chain; return raw fiber nodes (for navigation).\n */\nexport function getOwnerFibers(fiber: Fiber | null | undefined): Fiber[] {\n const fibers: Fiber[] = [];\n let current = fiber?._debugOwner || null;\n while (current) {\n if (isUserComponent(current)) fibers.push(current);\n current = current._debugOwner || null;\n }\n return fibers;\n}\n\n/**\n * Recursively walk fiber.child/sibling to find the first host DOM element rendered by this subtree.\n */\nexport function getFiberDomNode(fiber: Fiber | null | undefined): Element | null {\n if (!fiber) return null;\n // Host fiber \u2014 stateNode is a DOM element\n if (fiber.stateNode instanceof Element) return fiber.stateNode;\n // Walk children\n let child = fiber.child || null;\n while (child) {\n const found = getFiberDomNode(child);\n if (found) return found;\n child = child.sibling || null;\n }\n return null;\n}\n\n/**\n * Walk fiber.return chain; collect all user component parents with metadata.\n */\nexport function getReturnComponentChain(fiber: Fiber | null | undefined): OwnerEntry[] {\n const chain: OwnerEntry[] = [];\n let current = fiber?.return || null;\n while (current) {\n if (isUserComponent(current)) {\n chain.push({\n name: getDisplayName(current.type),\n key: current.key || null,\n props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],\n ...getHookSummary(current),\n source: describeFiberSource(current),\n });\n }\n current = current.return || null;\n }\n return chain;\n}\n\n/**\n * Walk fiber.return chain; return raw fiber nodes (for navigation).\n */\nexport function getReturnComponentFibers(fiber: Fiber | null | undefined): Fiber[] {\n const fibers: Fiber[] = [];\n let current = fiber?.return || null;\n while (current) {\n if (isUserComponent(current)) fibers.push(current);\n current = current.return || null;\n }\n return fibers;\n}\n\n/**\n * Normalize source location object to canonical shape.\n */\nexport function describeSource(source: SourceLocation | null | undefined): DescribedSource | null {\n if (!source) return null;\n return {\n fileName: source.fileName || null,\n lineNumber: source.lineNumber || null,\n columnNumber: source.columnNumber || null,\n };\n}\n\n/**\n * Read a fiber source across React versions.\n *\n * React <=18 exposes `_debugSource`. React 19 exposes `_debugStack`; its first\n * non-vendor frame is returned synchronously and may later be upgraded from the\n * bundle position to the original source position by `resolveFiberSource`.\n */\nexport function describeFiberSource(fiber: Fiber | null | undefined): DescribedSource | null {\n return describeSource(readFiberSource(fiber));\n}\n\nexport async function resolveFiberSource(fiber: Fiber | null | undefined): Promise<DescribedSource | null> {\n return describeSource(await resolveSourceLocation(fiber));\n}\n\n/**\n * Describe a fiber + DOM node pair; used for inspection UI.\n */\nexport function describeFiberTarget(fiber: Fiber | null | undefined, domNode: Element | null | undefined): FiberTarget | null {\n if (!fiber && !domNode) return null;\n return {\n domTag: domNode?.tagName?.toLowerCase() || null,\n domText: domNode?.textContent?.trim().slice(0, 60) || '',\n fiberType: getDisplayName(fiber?.type) || getDisplayName(fiber?.elementType) || null,\n source: describeFiberSource(fiber),\n };\n}\n\n/**\n * Full component description: name, props, hook summary, source, owners, parents.\n */\nexport function describeComponent(fiber: Fiber | null | undefined): ComponentDescription | null {\n if (!fiber) return null;\n return {\n name: getDisplayName(fiber.type),\n key: fiber.key || null,\n props: fiber.memoizedProps || {},\n ...getHookSummary(fiber),\n source: describeFiberSource(fiber),\n stateNode: fiber.stateNode instanceof Element ? fiber.stateNode.tagName.toLowerCase() : null,\n owners: getOwnerChain(fiber),\n parents: getReturnComponentChain(fiber),\n };\n}\n", "// React fiber constants and structural comparisons used by the commit detector.\n\nimport { getFiberSourceCandidate } from '../chippy-bridge/fiber-source';\n\nexport type ComponentType =\n | Function\n | symbol\n | {\n // ForwardRef/memo/etc may be objects.\n $$typeof?: symbol;\n render?: Function;\n displayName?: string;\n name?: string;\n };\n\ninterface StoreCheck {\n getSnapshot: () => unknown;\n value: unknown;\n}\n\nexport interface Fiber {\n tag: number;\n type: ComponentType;\n alternate: Fiber | null;\n return: Fiber | null;\n child: Fiber | null;\n sibling: Fiber | null;\n flags: number;\n subtreeFlags: number;\n lanes: number;\n memoizedState: Hook | null;\n memoizedProps?: unknown;\n pendingProps?: unknown;\n updateQueue: { stores: StoreCheck[] | null } | null;\n /**\n * React populates this array during the render phase with fibers that will be\n * deleted in the commit phase. Present when subtreeFlags & ChildDeletion !== 0.\n * Only available in React 18+ (replaces the older effectList pattern).\n */\n deletions?: Fiber[] | null;\n\n // Dev-only / optional debug fields (not always present).\n elementType?: unknown;\n _debugSource?: { fileName: string; lineNumber: number };\n _debugStack?: unknown;\n _debugHookTypes?: string[];\n}\n\nexport interface Hook {\n memoizedState: unknown;\n baseState: unknown;\n baseQueue: unknown;\n queue: HookQueue | null;\n next: Hook | null;\n}\n\ninterface HookQueue {\n dispatch?: Function;\n value?: unknown;\n getSnapshot?: Function;\n lastRenderedState?: unknown;\n}\n\nexport interface DepOrigin {\n type: ComponentType;\n hookIndex: number;\n depth: number;\n hookType: 'useCallback' | 'useMemo';\n}\n\nexport interface ThrashInfo {\n hookIndex: number;\n hookType: 'effect' | 'callback' | 'memo';\n deps: unknown[];\n changedDepIndices: number[];\n changedDepOrigins: (DepOrigin | null)[];\n effectTag?: number;\n memoValue?: unknown;\n /** The hook body function (for useCallback) or memoized value (for useMemo) */\n hookBody?: Function | unknown;\n}\n\nexport type ChangedHook =\n | {\n prevValue: unknown;\n currentValue: unknown;\n info?: string; // For structural changes\n }\n | 'repeat'; // Entire object replaced with string if identical to previous commit\n\nexport interface StateChangeResult {\n hasChange: boolean;\n actionable: boolean;\n changedIndices: number[];\n changedHooks: ChangedHook[];\n /** True if any of the changes came from useSyncExternalStore */\n hasExternalStoreChange: boolean;\n}\n\nexport interface ThrashingResult {\n effects: ThrashInfo[];\n callbacks: ThrashInfo[];\n memos: ThrashInfo[];\n}\n\n// React fiber flags (React 18.3.1)\nexport const PerformedWork = 1;\n\n// Flags relevant to filtering out non-preventable work from collateral counts.\n// - Placement: mount/insert work (initial render)\n// - Hydrating: work done while hydrating server-rendered markup\n// These are stable for React 18.x.\nexport const Placement = 2;\nexport const Hydrating = 4096;\n\n// Special sentinel values for hook change detection\n\n// Component fiber tags for React 18.3.1 (stable within 18.x)\n// 0: FunctionComponent, 1: ClassComponent, 2: IndeterminateComponent\n// 11: MemoComponent, 14: SimpleMemoComponent, 15: ForwardRef\nconst COMPONENT_TAGS = new Set([0, 1, 2, 11, 14, 15]);\n\n// Context Provider tag\nconst CONTEXT_PROVIDER_TAG = 10;\n\n// Suspense fiber tag (React 18.3.1 \u2014 stable within 18.x)\nexport const SuspenseTag = 13;\n\n// ChildDeletion \u2014 set on fiber.flags (NOT subtreeFlags) when the fiber has deletions[].\n// Value 16 is stable for React 18.3.1.\nexport const ChildDeletion = 16;\n\n// Update \u2014 set on fiber.flags when the fiber received an update this commit.\n// This is the flag React puts on Suspense fibers when they transition state.\n// Value 4 is stable for React 18.3.1.\nexport const Update = 4;\n\n// DidCapture \u2014 render-phase flag, cleared before commit. Do NOT check at commit time.\n// Kept for documentation. Value 128 in React 18.3.1.\nexport const DidCapture = 128;\n\nexport function isComponentFiber(tag: number): boolean {\n return COMPONENT_TAGS.has(tag);\n}\n\nexport function isContextProviderFiber(tag: number): boolean {\n return tag === CONTEXT_PROVIDER_TAG;\n}\n\nexport function isSuspenseFiber(tag: number): boolean {\n return tag === SuspenseTag;\n}\n\n/**\n * Detect whether a Suspense boundary transitioned between suspended/unsuspended states.\n *\n * Suspense memoizedState is:\n * null \u2192 primary content is showing (not suspended)\n * non-null (object) \u2192 boundary is showing the fallback (suspended/dehydrated)\n *\n * Returns:\n * { entered: true } \u2014 boundary just entered fallback\n * - null \u2192 non-null (update path)\n * - no alternate + memoizedState non-null (first render directly to fallback)\n * { left: true } \u2014 boundary just left fallback / revealed primary (non-null \u2192 null)\n * null \u2014 no state change (both null, both non-null, or first render not suspended)\n *\n * A Suspense that renders without delay never enters fallback, so memoizedState stays\n * null \u2192 returns null \u2192 correctly ignored by the detector.\n */\nexport function didSuspenseStateChange(\n fiber: Fiber\n): { entered: boolean; left: boolean } | null {\n const currSuspended = fiber.memoizedState !== null;\n\n if (!fiber.alternate) {\n // First render of this Suspense boundary.\n // If it went directly to fallback (memoizedState non-null), that IS an enter.\n // If it rendered primary content (memoizedState null), no transition.\n if (currSuspended) {\n return { entered: true, left: false };\n }\n return null;\n }\n\n const prevSuspended = fiber.alternate.memoizedState !== null;\n if (prevSuspended === currSuspended) return null;\n return { entered: currSuspended, left: prevSuspended };\n}\n\n/**\n * Check if a Context Provider's value changed.\n */\nexport function didContextValueChange(fiber: Fiber): boolean {\n if (fiber.tag !== CONTEXT_PROVIDER_TAG || !fiber.alternate) return false;\n\n type FiberWithProps = Fiber & { memoizedProps?: { value?: unknown } };\n const alternateFiber: FiberWithProps = fiber.alternate;\n const currentFiber: FiberWithProps = fiber;\n\n const prevProps = alternateFiber.memoizedProps;\n const currentProps = currentFiber.memoizedProps;\n\n return prevProps && currentProps && !Object.is(prevProps.value, currentProps.value);\n}\n\n// Detect React built-in fiber types (Suspense/Offscreen/etc).\n// IMPORTANT: Do NOT use component names. User components can be named anything.\n// In React 18, built-ins like Suspense/Offscreen are represented as symbols.\nexport function isReactInternalComponentType(type: ComponentType): boolean {\n return typeof type === 'symbol';\n}\n\nexport function getComponentName(fiber: Fiber): string {\n type TypeWithName = {\n displayName?: string;\n name?: string;\n };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const type = fiber.type as TypeWithName;\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const elementType = fiber.elementType as TypeWithName;\n const named = (\n type?.displayName ||\n type?.name ||\n elementType?.displayName ||\n elementType?.name\n );\n if (named) return named;\n const source = getFiberSourceCandidate(fiber);\n return (source && `<${source.fileName}:${source.lineNumber}>`) || 'Anonymous';\n}\n\n/**\n * Check if hook is a state hook (useState/useReducer/useSyncExternalStore).\n *\n * All hooks share a base shape. The reliable differentiators:\n * - useState/useReducer: queue.dispatch is a function\n * - useSyncExternalStore: queue has 'value' and 'getSnapshot' properties\n */\nexport function isStateHook(hook: Hook): boolean {\n const queue = hook.queue;\n if (!queue) return false;\n\n return typeof queue.dispatch === 'function' || ('value' in queue && 'getSnapshot' in queue);\n}\n\n/** True if the hook is a useSyncExternalStore-like hook. */\nexport function isExternalStoreHook(hook: Hook): boolean {\n const queue = hook.queue;\n return (\n queue && typeof queue.dispatch !== 'function' && 'value' in queue && 'getSnapshot' in queue\n );\n}\n\n/**\n * Compare hook chains to detect state changes.\n *\n * IMPORTANT: Hook indices are React internal memoizedState indices, NOT code order.\n * `useContext` hooks are not in memoizedState, causing index misalignment.\n */\nexport function detectHookStateChanges(\n prevHookChain: Hook | null,\n currentHookChain: Hook | null\n): StateChangeResult {\n if (prevHookChain === currentHookChain) {\n return {\n hasChange: false,\n actionable: false,\n changedIndices: [],\n changedHooks: [],\n hasExternalStoreChange: false,\n };\n }\n\n let prevHook = prevHookChain;\n let currentHook = currentHookChain;\n let memoizedStateIndex = 0;\n const changedIndices: number[] = [];\n const changedHooks: ChangedHook[] = [];\n let hasExternalStoreChange = false;\n\n while (prevHook && currentHook) {\n if (isStateHook(currentHook)) {\n const prevValueRaw =\n prevHook.memoizedState === undefined\n ? prevHook.queue?.lastRenderedState\n : prevHook.memoizedState;\n const curValueRaw =\n currentHook.memoizedState === undefined\n ? currentHook.queue?.lastRenderedState\n : currentHook.memoizedState;\n\n if (!Object.is(prevValueRaw, curValueRaw)) {\n const isExternal = isExternalStoreHook(currentHook);\n if (isExternal) hasExternalStoreChange = true;\n\n changedIndices.push(memoizedStateIndex);\n changedHooks.push({\n prevValue: prevValueRaw,\n currentValue: curValueRaw,\n ...(isExternal && { isExternalStore: true }),\n });\n }\n }\n\n prevHook = prevHook.next;\n currentHook = currentHook.next;\n memoizedStateIndex++;\n }\n\n return {\n hasChange: changedIndices.length > 0,\n actionable: changedIndices.length > 0,\n changedIndices,\n changedHooks,\n hasExternalStoreChange,\n };\n}\n\n/**\n * Detect thrashing for hooks with deps: useEffect/useLayoutEffect/useInsertionEffect,\n * useCallback and useMemo.\n */\nexport function detectHookThrashing(\n prevHookChain: Hook | null,\n currentHookChain: Hook | null,\n mode: 'non-primitive' | 'all' = 'non-primitive',\n valueProvenance: WeakMap<object | Function, DepOrigin> | null = null\n): ThrashingResult {\n if (prevHookChain === currentHookChain) {\n return { effects: [], callbacks: [], memos: [] };\n }\n\n const thrashedEffects: ThrashInfo[] = [];\n const thrashedCallbacks: ThrashInfo[] = [];\n const thrashedMemos: ThrashInfo[] = [];\n\n let prevHook = prevHookChain;\n let currentHook = currentHookChain;\n let hookIndex = 0;\n\n while (prevHook && currentHook) {\n if (!isStateHook(currentHook)) {\n const prevValue = prevHook.memoizedState;\n const currentValue = currentHook.memoizedState;\n\n let prevDeps: unknown[] | null = null;\n let currentDeps: unknown[] | null = null;\n let hookType: ThrashInfo['hookType'] | null = null;\n\n // Effect hooks store an object with `deps`.\n if (\n prevValue &&\n currentValue &&\n typeof prevValue === 'object' &&\n typeof currentValue === 'object' &&\n 'deps' in prevValue &&\n 'deps' in currentValue &&\n 'tag' in currentValue\n ) {\n type EffectHook = { deps: unknown[] };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n prevDeps = (prevValue as EffectHook).deps;\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n currentDeps = (currentValue as EffectHook).deps;\n hookType = 'effect';\n }\n // useMemo/useCallback store [result, deps].\n else if (\n Array.isArray(prevValue) &&\n Array.isArray(currentValue) &&\n prevValue.length === 2 &&\n currentValue.length === 2 &&\n Array.isArray(prevValue[1]) &&\n Array.isArray(currentValue[1])\n ) {\n prevDeps = prevValue[1];\n currentDeps = currentValue[1];\n hookType = typeof currentValue[0] === 'function' ? 'callback' : 'memo';\n }\n\n if (hookType && Array.isArray(prevDeps) && Array.isArray(currentDeps)) {\n const changedDepIndices: number[] = [];\n const changedDepOrigins: (DepOrigin | null)[] = [];\n\n // eslint-disable-next-line @atlassian/perf-linting/no-quadratic-complexity\n for (let j = 0; j < currentDeps.length; j++) {\n if (!Object.is(prevDeps[j], currentDeps[j])) {\n changedDepIndices.push(j);\n\n let origin: DepOrigin | null = null;\n const dep = currentDeps[j];\n if (\n valueProvenance &&\n dep != null &&\n (typeof dep === 'object' || typeof dep === 'function')\n ) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n origin = valueProvenance.get(dep as object | ((...args: never[]) => unknown)) || null;\n }\n changedDepOrigins.push(origin);\n }\n }\n\n let shouldReport = changedDepIndices.length > 0;\n if (shouldReport && mode === 'non-primitive') {\n // Only report if at least one non-primitive dep changed.\n // eslint-disable-next-line @atlassian/perf-linting/no-quadratic-complexity\n shouldReport = changedDepIndices.some((idx) => {\n const dep = currentDeps[idx];\n const t = typeof dep;\n return !(\n dep == null ||\n t === 'string' ||\n t === 'number' ||\n t === 'boolean' ||\n t === 'symbol' ||\n t === 'bigint'\n );\n });\n }\n\n if (shouldReport) {\n const thrashInfo: ThrashInfo = {\n hookIndex,\n hookType,\n deps: currentDeps,\n changedDepIndices,\n changedDepOrigins,\n };\n\n if (hookType === 'effect') {\n type EffectValue = { tag?: number; create?: Function };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const effectValue = currentValue as EffectValue;\n thrashInfo.effectTag = effectValue.tag;\n // Capture the effect function itself (the create function)\n if (effectValue.create) {\n thrashInfo.hookBody = effectValue.create;\n }\n thrashedEffects.push(thrashInfo);\n } else if (hookType === 'callback') {\n type CallbackValue = [Function, unknown[]];\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const callbackFn = (currentValue as CallbackValue)[0];\n // Capture the callback function itself\n thrashInfo.hookBody = callbackFn;\n thrashedCallbacks.push(thrashInfo);\n } else {\n type MemoValue = [unknown, unknown[]];\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const memoValue = (currentValue as MemoValue)[0];\n thrashInfo.memoValue = memoValue;\n // For useMemo, also expose the memoized value as hookBody\n thrashInfo.hookBody = memoValue;\n thrashedMemos.push(thrashInfo);\n }\n }\n }\n }\n\n prevHook = prevHook.next;\n currentHook = currentHook.next;\n hookIndex++;\n }\n\n return {\n effects: thrashedEffects,\n callbacks: thrashedCallbacks,\n memos: thrashedMemos,\n };\n}\n\nexport function didFiberUpdate(fiber: Fiber): boolean {\n // eslint-disable-next-line no-bitwise\n return (fiber.flags & PerformedWork) !== 0;\n}\n\n/**\n * Detect if fiber has actionable hook state changes by comparing with fiber.alternate.\n *\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 \u26A0\uFE0F WARNING: fiber.alternate COMPARISON IS UNRELIABLE \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * This function compares fiber.memoizedState vs fiber.alternate.memoizedState.\n *\n * THE PROBLEM:\n * fiber.alternate is NOT always \"state from the previous commit\".\n * It's \"state from last time this fiber was used as work-in-progress\".\n *\n * If a component re-renders due to parent/context changes (not self-update),\n * its alternate may be stale - from 1 commit ago, or 100 commits ago!\n *\n * This causes FALSE POSITIVES: we detect \"state changed\" when it didn't\n * change in THIS commit - it changed in some PAST commit.\n *\n * WHEN THIS FUNCTION IS RELIABLE:\n * \u2713 Component called setState (it IS the initiator)\n * \u2713 Component has useSyncExternalStore that just triggered\n * \u2713 Used alongside memoizedUpdaters validation\n *\n * WHEN THIS FUNCTION IS UNRELIABLE:\n * \u2717 Component re-rendered due to parent update\n * \u2717 Component re-rendered due to context change\n * \u2717 Component was memoized and skipped N commits\n * \u2717 Any scenario where alternate wasn't recently updated\n *\n * RECOMMENDED USAGE:\n * - In development: Use memoizedUpdaters to verify component is an initiator,\n * THEN use this function to get hook change details.\n * - In production: Use lastSeenHookState tracking (see detector.tsx)\n * instead of relying on fiber.alternate.\n *\n * See detector.tsx for the full explanation of the \"swap\" problem and solution.\n */\nexport function didFiberSelfUpdate(fiber: Fiber): StateChangeResult | null {\n // Early exit: only check fibers that actually rendered (have PerformedWork flag)\n if (!didFiberUpdate(fiber)) return null;\n\n const alt = fiber.alternate;\n if (!alt) return null;\n\n const prevState = alt.memoizedState;\n const currentState = fiber.memoizedState;\n\n const result = detectHookStateChanges(prevState, currentState);\n return result.hasChange ? result : null;\n}\n\nexport function isDevelopment(): boolean {\n return process.env.NODE_ENV !== 'production';\n}\n", "/**\n * Types and helpers for writing a Chippy plugin. `chippy pack` bundles a\n * plugin module's default export into a custom skill and extension build.\n */\nimport type { ChippyPlugin } from '../plugins/index';\n\nexport type { ChippyPlugin, PluginStatus } from '../plugins/index';\nexport type {\n ContextBoundaryKind,\n ContextContribution,\n ContextField,\n ContextProvider,\n} from '../layout-lab/marker-context';\nexport type { Fiber } from '../layout-lab/fiber-walk';\nexport type {\n CommitMetricsAdapter,\n CommitMetricsDelta,\n CommitMetricsMeasurement,\n CommitMetricsRoot,\n CommitMetricsSnapshot,\n} from '../avalanche/commit-metrics-adapter';\n/** Display name of a fiber's `type`: function, class, memo, or forwardRef. */\nexport { getDisplayName } from '../layout-lab/fiber-walk';\n/** Fiber name helpers that commit metrics adapters use for hook ancestry. */\nexport { getComponentName, isComponentFiber } from '../avalanche/internals';\n\n/** Identity helper that type-checks a plugin definition. */\nexport function definePlugin(plugin: ChippyPlugin): ChippyPlugin {\n return plugin;\n}\n"],
5
+ "mappings": ";AAOA,IAAM,SAAS;AACf,IAAM,WAAW,IAAI,UAAU,GAAG,EAAE,KAAK,EAAE;AAC3C,SAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,GAAG;AACrD,WAAS,OAAO,WAAW,KAAK,CAAC,IAAI;AACvC;;;ACoCA,IAAM,WAAW;AACjB,IAAM,WAAW;AACjB,IAAM,uBAAuB;AAMtB,SAAS,iBAAiB,OAA6B;AAC5D,QAAM,SAAuB,CAAC;AAE9B,aAAW,OAAO,MAAM,MAAM,IAAI,GAAG;AACnC,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,SAAS,MAAM,KAAK,SAAS,WAAW,EAAG;AAC/C,UAAM,QAAQ,SAAS,KAAK,IAAI,KAAK,SAAS,KAAK,IAAI;AACvD,QAAI,CAAC,MAAO;AAEZ,UAAM,CAAC,EAAE,MAAM,MAAM,IAAI,aAAa,IAAI,eAAe,EAAE,IAAI;AAC/D,QAAI,QAAQ,MAAM,IAAI,WAAW,OAAO,EAAG;AAC3C,UAAM,KAAK,OAAO,UAAU;AAC5B,UAAM,SAAS,OAAO,YAAY;AAClC,QAAI,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,OAAO,SAAS,MAAM,KAAK,KAAK,EAAG;AAChE,WAAO,KAAK;AAAA,MACV;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,GAAI,SAAS,UAAa,SAAS,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAAA,IAChE,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEO,SAAS,aAAa,MAAuB;AAClD,SACE,KAAK,SAAS,cAAc,KAC5B,KAAK,SAAS,cAAc,KAC5B,KAAK,SAAS,SAAS;AAE3B;AAEA,SAAS,YAAY,OAAwC;AAC3D,SAAO,EAAE,UAAU,MAAM,KAAK,YAAY,MAAM,MAAM,cAAc,MAAM,OAAO;AACnF;AAEA,SAAS,qBAAqB,UAAiD;AAC7E,SAAO,EAAE,UAAU,SAAS,QAAQ,YAAY,SAAS,MAAM,cAAc,SAAS,OAAO;AAC/F;AAEO,SAAS,iBACd,QACA,aAC4B;AAC5B,aAAW,SAAS,QAAQ;AAC1B,QAAI,aAAa,MAAM,GAAG,EAAG;AAC7B,UAAM,WAAW,YAAY,KAAK;AAClC,QAAI,aAAa,KAAM,QAAO,YAAY,KAAK;AAC/C,QAAI,aAAa,SAAS,MAAM,EAAG;AACnC,WAAO,qBAAqB,QAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAiHA,SAAS,cAAc,OAA4C;AACjE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,SAAS;AACf,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,OAAO,eAAe,SAAU,QAAO;AACzF,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO;AAAA,IACnB,cAAc,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AAAA,EAChF;AACF;AAEA,SAAS,eAAe,OAAgD;AACtE,QAAM,SAAS,cAAc,MAAM,YAAY;AAC/C,MAAI,OAAQ,QAAO;AACnB,QAAM,OAAO,MAAM;AACnB,SAAO,SAAS,OAAO,SAAS,YAAY,OAAO,SAAS,cACxD,cAAe,KAAoC,YAAY,IAC/D;AACN;AAEO,SAAS,oBAAoB,OAAqD;AACvF,MAAI,CAAC,SAAS,CAAC,MAAM,eAAe,OAAO,MAAM,gBAAgB,SAAU,QAAO,CAAC;AACnF,QAAM,QAAS,MAAM,YAAoC;AACzD,MAAI,OAAO,UAAU,YAAY,UAAU,GAAI,QAAO,CAAC;AACvD,QAAM,aAA2B,CAAC;AAClC,aAAW,SAAS,iBAAiB,KAAK,GAAG;AAC3C,QAAI,aAAa,MAAM,GAAG,EAAG;AAC7B,eAAW,KAAK,KAAK;AACrB,QAAI,WAAW,UAAU,qBAAsB;AAAA,EACjD;AACA,SAAO;AACT;AAGO,SAAS,wBAAwB,OAAmE;AACzG,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,eAAe,KAAK;AACrC,MAAI,SAAU,QAAO;AACrB,SAAO,iBAAiB,oBAAoB,KAAK,GAAG,MAAM,IAAI;AAChE;;;ACnKO,SAAS,eAAe,MAA8B;AAC3D,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,MAAM;AACZ,SAAQ,IAAI,eAA2B,IAAI,QAAmB;AAChE;;;ACkBA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AAqB7C,SAAS,iBAAiB,KAAsB;AACrD,SAAO,eAAe,IAAI,GAAG;AAC/B;AAsEO,SAAS,iBAAiB,OAAsB;AAMrD,QAAM,OAAO,MAAM;AAEnB,QAAM,cAAc,MAAM;AAC1B,QAAM,QACJ,MAAM,eACN,MAAM,QACN,aAAa,eACb,aAAa;AAEf,MAAI,MAAO,QAAO;AAClB,QAAM,SAAS,wBAAwB,KAAK;AAC5C,SAAQ,UAAU,IAAI,OAAO,QAAQ,IAAI,OAAO,UAAU,OAAQ;AACpE;;;AC5MO,SAAS,aAAa,QAAoC;AAC/D,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,79 @@
1
+ // TODO: This is a deliberately lossy UI signature. Commits that differ
2
+ // only in omitted timing, rendered, ATL, or other diagnostic fields collapse
3
+ // into one "repeat" count; either expose that scope in the UI or expand the key
4
+ // before treating grouped rows as equivalent behavior.
5
+ const PATTERN_STAT_KEYS = [
6
+ 'collateral',
7
+ 'restarts',
8
+ 'performedWork',
9
+ 'effects',
10
+ 'memos',
11
+ 'callbacks',
12
+ 'contextUpdates',
13
+ 'unmountedComponents',
14
+ 'unmountedSubtrees',
15
+ ];
16
+
17
+ function count(value) {
18
+ const number = Number(value);
19
+ return Number.isFinite(number) ? number : 0;
20
+ }
21
+
22
+ function actionIdentity(row) {
23
+ const action = row?.latestAction;
24
+ return [
25
+ action?.action || '',
26
+ action?.dom?.nearestTestId || '',
27
+ action?.targetFiber?.name || '',
28
+ action?.dom?.tag || '',
29
+ ];
30
+ }
31
+
32
+ function patternKey(row) {
33
+ return JSON.stringify([
34
+ count(row?.sourceCount),
35
+ ...PATTERN_STAT_KEYS.map((key) => count(row?.stat?.[key])),
36
+ ...actionIdentity(row),
37
+ ]);
38
+ }
39
+
40
+ export function groupCommitRows(rows) {
41
+ const groups = new Map();
42
+
43
+ for (const row of rows || []) {
44
+ const key = patternKey(row);
45
+ const commitId = count(row?.commitId);
46
+ const current = groups.get(key);
47
+
48
+ if (!current) {
49
+ groups.set(key, {
50
+ key,
51
+ count: 1,
52
+ firstCommitId: commitId,
53
+ lastCommitId: commitId,
54
+ representative: row,
55
+ });
56
+ continue;
57
+ }
58
+
59
+ current.count += 1;
60
+ current.firstCommitId = Math.min(current.firstCommitId, commitId);
61
+ current.lastCommitId = Math.max(current.lastCommitId, commitId);
62
+ if (commitId >= count(current.representative?.commitId)) current.representative = row;
63
+ }
64
+
65
+ return Array.from(groups.values()).sort((a, b) => b.lastCommitId - a.lastCommitId);
66
+ }
67
+
68
+ export function describeCommitAction(row) {
69
+ const action = row?.latestAction;
70
+ if (!action?.action) return 'render';
71
+ const target = action.dom?.nearestTestId || action.targetFiber?.name || action.dom?.tag;
72
+ return target ? `${action.action} · ${target}` : action.action;
73
+ }
74
+
75
+ export function commitSpan(group) {
76
+ if (!group) return '—';
77
+ if (group.firstCommitId === group.lastCommitId) return `#${group.lastCommitId}`;
78
+ return `#${group.firstCommitId}–#${group.lastCommitId}`;
79
+ }
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/sync-chippy-brand.mjs from extension/logo.svg.
2
+ import React from 'react';
3
+
4
+ export function ChippyLogo() {
5
+ return <span style={{ display: 'inline-flex' }} dangerouslySetInnerHTML={{ __html: "<svg data-chippy-logo aria-hidden=\"true\" width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" fill=\"none\">\n <defs>\n <linearGradient id=\"chippy-trace-blue\" x1=\"4\" y1=\"25\" x2=\"28\" y2=\"7\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#1558BC\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n <linearGradient id=\"chippy-trace-light\" x1=\"4\" y1=\"9\" x2=\"27\" y2=\"9\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#85B8FF\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n </defs>\n <style>\n @keyframes chippy-trace-reveal { from { opacity: .45; transform: translateY(-1.5px); } to { opacity: 1; transform: translateY(0); } }\n .chippy-trace-cut { animation: chippy-trace-reveal 650ms ease-out both; }\n @media (prefers-reduced-motion: reduce) { .chippy-trace-cut { animation: none; } }\n </style>\n <path d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4v5.2l5 3.1 5-3.1 7 4.4L16 30.5 4 23Z\" fill=\"url(#chippy-trace-blue)\"/>\n <path class=\"chippy-trace-cut\" d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4Z\" fill=\"url(#chippy-trace-light)\"/>\n <path d=\"m11 18.6 5 3.1 5-3.1 7 4.4-12 7.5-5-3.1Z\" fill=\"#1868DB\"/>\n</svg>" }} />;
6
+ }