@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,38 @@
1
+ // List active modern React Context providers on the page.
2
+ // Optional args:
3
+ // --name <regex> filter by Context displayName / name (case-insensitive)
4
+ // --max <N> cap results (default 500)
5
+
6
+ const { findFibers, fiberInfo, ancestorFibers, getDisplayName } = ctx.helpers;
7
+ const nameRe = ctx.args.name ? new RegExp(String(ctx.args.name), 'i') : null;
8
+ const max = Number(ctx.args.max) || 500;
9
+
10
+ // Provider fibers: tag === 10 in modern React, type._context exists.
11
+ const fibers = findFibers((f) => {
12
+ const t = f?.type;
13
+ if (!t || typeof t !== 'object') return false;
14
+ // ContextProvider: type._context, $$typeof matches React.createContext output
15
+ return !!t._context;
16
+ }, { max: max * 4 });
17
+
18
+ const seen = new Map(); // key = contextId → { name, count, samples[] }
19
+ for (const f of fibers) {
20
+ const ctxObj = f.type._context;
21
+ const name = ctxObj.displayName || ctxObj._currentValue?.displayName || ctxObj?.Provider?.displayName || null;
22
+ if (nameRe && !nameRe.test(name || '')) continue;
23
+ const id = name || ('anon@' + Object.keys(seen).length);
24
+ let rec = seen.get(id);
25
+ if (!rec) { rec = { name, count: 0, samples: [] }; seen.set(id, rec); }
26
+ rec.count++;
27
+ if (rec.samples.length < 3) {
28
+ rec.samples.push({
29
+ provider: fiberInfo(f),
30
+ owner: f._debugOwner ? fiberInfo(f._debugOwner) : null,
31
+ ancestors: ancestorFibers(f, 6).map((a) => a.name),
32
+ });
33
+ }
34
+ }
35
+
36
+ const rows = Array.from(seen.values()).sort((a, b) => b.count - a.count);
37
+ ctx.log.push('matched providers: ' + rows.length);
38
+ return { totalProviders: rows.length, top: rows.slice(0, max) };
@@ -0,0 +1,47 @@
1
+ // Find class components using the legacy React context API.
2
+ // Producer: class with `childContextTypes`
3
+ // Consumer: class with `contextTypes`
4
+ // Args:
5
+ // --name <regex> filter by component displayName / name
6
+ // --kind <producer|consumer|both> default both
7
+ // --max <N> default 500
8
+
9
+ const { findFibers, fiberInfo, ancestorFibers, getDisplayName } = ctx.helpers;
10
+ const nameRe = ctx.args.name ? new RegExp(String(ctx.args.name), 'i') : null;
11
+ const kind = String(ctx.args.kind || 'both');
12
+ const max = Number(ctx.args.max) || 500;
13
+
14
+ function classifies(t) {
15
+ if (!t) return null;
16
+ const isProducer = !!t.childContextTypes && Object.keys(t.childContextTypes).length > 0;
17
+ const isConsumer = !!t.contextTypes && Object.keys(t.contextTypes).length > 0;
18
+ if (!isProducer && !isConsumer) return null;
19
+ return { isProducer, isConsumer };
20
+ }
21
+
22
+ const fibers = findFibers((f) => !!classifies(f?.type), { max: max * 4 });
23
+ const groups = { producers: new Map(), consumers: new Map() };
24
+ for (const f of fibers) {
25
+ const c = classifies(f.type);
26
+ const name = getDisplayName(f) || '<anonymous>';
27
+ if (nameRe && !nameRe.test(name)) continue;
28
+ if (c.isProducer && (kind === 'producer' || kind === 'both')) {
29
+ let rec = groups.producers.get(name);
30
+ if (!rec) { rec = { name, count: 0, contextKeys: Object.keys(f.type.childContextTypes), samples: [] }; groups.producers.set(name, rec); }
31
+ rec.count++;
32
+ if (rec.samples.length < 3) rec.samples.push({ self: fiberInfo(f), owner: f._debugOwner ? fiberInfo(f._debugOwner) : null });
33
+ }
34
+ if (c.isConsumer && (kind === 'consumer' || kind === 'both')) {
35
+ let rec = groups.consumers.get(name);
36
+ if (!rec) { rec = { name, count: 0, contextKeys: Object.keys(f.type.contextTypes), samples: [] }; groups.consumers.set(name, rec); }
37
+ rec.count++;
38
+ if (rec.samples.length < 3) rec.samples.push({ self: fiberInfo(f), owner: f._debugOwner ? fiberInfo(f._debugOwner) : null });
39
+ }
40
+ }
41
+
42
+ const sortRows = (m) => Array.from(m.values()).sort((a, b) => b.count - a.count).slice(0, max);
43
+ return {
44
+ scannedFibers: fibers.length,
45
+ producers: sortRows(groups.producers),
46
+ consumers: sortRows(groups.consumers),
47
+ };
@@ -0,0 +1,83 @@
1
+ // Find fibers carrying a given prop, optionally with a matching value.
2
+ // Args:
3
+ // --prop <name> REQUIRED. Prop key to look for (exact match).
4
+ // --value <regex> OPTIONAL. Regex matched against String(propValue) (case-insensitive). Objects/functions become tag strings.
5
+ // --name <regex> OPTIONAL. Filter by component displayName regex (case-insensitive).
6
+ // --file <regex> OPTIONAL. Filter by _debugSource.fileName regex (case-insensitive).
7
+ // --max <N> cap result rows (default 200)
8
+ // --groupBy <name|file|none> default 'name'. Groups identical components into one row with samples.
9
+ // --depth <N> depth-trim for sample value (default 2)
10
+
11
+ const { walkAll, fiberInfo, getDisplayName } = ctx.helpers;
12
+ if (!ctx.args.prop) return { error: '--prop <name> is required' };
13
+ const propKey = String(ctx.args.prop);
14
+ const valueRe = ctx.args.value ? new RegExp(String(ctx.args.value), 'i') : null;
15
+ const nameRe = ctx.args.name ? new RegExp(String(ctx.args.name), 'i') : null;
16
+ const fileRe = ctx.args.file ? new RegExp(String(ctx.args.file), 'i') : null;
17
+ const max = Number(ctx.args.max) || 200;
18
+ const groupBy = ['name', 'file', 'none'].includes(String(ctx.args.groupBy)) ? String(ctx.args.groupBy) : 'name';
19
+ const depth = Number.isFinite(Number(ctx.args.depth)) ? Math.max(0, Number(ctx.args.depth) | 0) : 2;
20
+
21
+ function tagOf(v) {
22
+ if (v == null) return String(v);
23
+ const t = typeof v;
24
+ if (t === 'function') return '[Function ' + (v.name || 'anonymous') + ']';
25
+ if (t === 'object') return Array.isArray(v) ? '[Array len=' + v.length + ']' : '[Object]';
26
+ return String(v);
27
+ }
28
+
29
+ function trim(value, d) {
30
+ if (d <= 0 || value == null) {
31
+ if (value && typeof value === 'object') return Array.isArray(value) ? '[Array]' : '[Object]';
32
+ return value;
33
+ }
34
+ if (Array.isArray(value)) return value.slice(0, 10).map((v) => trim(v, d - 1));
35
+ if (typeof value === 'object') {
36
+ const out = {};
37
+ let n = 0;
38
+ for (const k of Object.keys(value)) {
39
+ if (n++ > 20) { out._truncated = true; break; }
40
+ try { out[k] = trim(value[k], d - 1); } catch (_) { out[k] = '[Unserializable]'; }
41
+ }
42
+ return out;
43
+ }
44
+ if (typeof value === 'function') return '[Function ' + (value.name || 'anonymous') + ']';
45
+ return value;
46
+ }
47
+
48
+ const groups = new Map();
49
+ let scanned = 0;
50
+ let matched = 0;
51
+
52
+ walkAll((f) => {
53
+ scanned++;
54
+ const props = f?.memoizedProps;
55
+ if (!props || typeof props !== 'object') return;
56
+ if (!Object.prototype.hasOwnProperty.call(props, propKey)) return;
57
+ const name = getDisplayName(f) || '<anonymous>';
58
+ if (nameRe && !nameRe.test(name)) return;
59
+ const file = f?._debugSource?.fileName || f?.type?._debugSource?.fileName || null;
60
+ if (fileRe && !fileRe.test(file || '')) return;
61
+ const v = props[propKey];
62
+ const tag = tagOf(v);
63
+ if (valueRe && !valueRe.test(tag) && !(typeof v === 'string' && valueRe.test(v))) return;
64
+ matched++;
65
+ const key = groupBy === 'name' ? name
66
+ : groupBy === 'file' ? (file || '<no-source>')
67
+ : (name + '|' + (file || '') + '|' + (f?._debugSource?.lineNumber || ''));
68
+ let rec = groups.get(key);
69
+ if (!rec) { rec = { key, name, file, line: f?._debugSource?.lineNumber || null, count: 0, samples: [] }; groups.set(key, rec); }
70
+ rec.count++;
71
+ if (rec.samples.length < 3) {
72
+ rec.samples.push({
73
+ self: fiberInfo(f),
74
+ owner: f._debugOwner ? fiberInfo(f._debugOwner) : null,
75
+ propValueTag: tag,
76
+ propValue: trim(v, depth),
77
+ });
78
+ }
79
+ });
80
+
81
+ const rows = Array.from(groups.values()).sort((a, b) => b.count - a.count).slice(0, max);
82
+ ctx.log.push('scanned: ' + scanned + ' matched: ' + matched + ' groups: ' + groups.size);
83
+ return { propKey, totalFibers: scanned, matched, groups: groups.size, top: rows };
@@ -0,0 +1,69 @@
1
+ // Capture a labeled before/after setStateCount delta in ONE call.
2
+ //
3
+ // This is the "lighthouse number" pattern as a first-class read: sample the cheap
4
+ // counter, run a bounded action (a click by testId, or just settle), sample again,
5
+ // return the delta. Answers "how many components fired setState during <phase>".
6
+ //
7
+ // METRIC IS OPTIONAL. The react-dom patch that emits __ATL_REACT_METRICS.setStateCount
8
+ // may or may not be present. If it is absent, wiped, or non-finite (NaN), this snippet
9
+ // returns { available:false, reason } and NEVER throws — callers must not rely on the
10
+ // metric being there.
11
+ //
12
+ // Args:
13
+ // --label <text> OPTIONAL. Human label for the phase (default "sample").
14
+ // --click <testId> OPTIONAL. data-testid to click between before/after samples.
15
+ // --clicks <n> OPTIONAL. repeat the click n times (default 1).
16
+ // --settleMs <n> OPTIONAL. ms to wait after the action before the after-sample
17
+ // (default 300). Also used as the whole wait when no --click.
18
+ //
19
+ // Output (available):
20
+ // { available:true, label, before, after, delta, clicked, settleMs }
21
+ // Output (unavailable):
22
+ // { available:false, label, reason }
23
+
24
+ function readSSC() {
25
+ const m = globalThis.__ATL_REACT_METRICS;
26
+ if (m == null) return { ok: false, reason: 'metrics absent (__ATL_REACT_METRICS is ' + String(m) + ')' };
27
+ const v = m.setStateCount;
28
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
29
+ return { ok: false, reason: 'setStateCount is ' + String(v) + ' (patch absent or seeded non-numeric)' };
30
+ }
31
+ return { ok: true, value: v };
32
+ }
33
+
34
+ const label = ctx.args.label ? String(ctx.args.label) : 'sample';
35
+ const settleMs = Number.isFinite(Number(ctx.args.settleMs)) ? Number(ctx.args.settleMs) : 300;
36
+ const clickId = ctx.args.click ? String(ctx.args.click) : null;
37
+ const clicks = Number(ctx.args.clicks) > 0 ? Number(ctx.args.clicks) : 1;
38
+
39
+ const b = readSSC();
40
+ if (!b.ok) {
41
+ ctx.log.push('setStateCount unavailable: ' + b.reason);
42
+ return { available: false, label, reason: b.reason };
43
+ }
44
+ const before = b.value;
45
+
46
+ let clicked = 0;
47
+ if (clickId) {
48
+ const sel = '[data-testid="' + clickId + '"]';
49
+ for (let i = 0; i < clicks; i++) {
50
+ const el = document.querySelector(sel);
51
+ if (el) { el.click(); clicked++; }
52
+ // small yield so React can flush between clicks
53
+ await new Promise((r) => setTimeout(r, Math.min(80, settleMs)));
54
+ }
55
+ if (clicked === 0) ctx.log.push('no element matched ' + sel);
56
+ }
57
+
58
+ await new Promise((r) => setTimeout(r, settleMs));
59
+
60
+ const a = readSSC();
61
+ if (!a.ok) {
62
+ // metric vanished mid-sample (host wiped it) — report honestly
63
+ return { available: false, label, reason: 'after-sample: ' + a.reason, before };
64
+ }
65
+ const after = a.value;
66
+
67
+ ctx.log.push(label + ': ' + before + ' -> ' + after + ' (delta ' + (after - before) + ')');
68
+
69
+ return { available: true, label, before, after, delta: after - before, clicked, settleMs };
@@ -0,0 +1,60 @@
1
+ // Read the host-owned React metrics object (__ATL_REACT_METRICS) — READ ONLY.
2
+ //
3
+ // Chippy READS this object; it does not create or seed it (the host/app owns it,
4
+ // same way Chippy reads any avalanche counter). The only new hook-related number
5
+ // here is `setStateCount` (useState/useReducer dispatches scheduled). Richer
6
+ // hook-update accounting lives in the separate `hook-tracker` snippet.
7
+ //
8
+ // Args:
9
+ // --keys <csv> OPTIONAL. Comma-separated counter names to return (default: all).
10
+ // --raw OPTIONAL. If present, return every own key regardless of type.
11
+ //
12
+ // Output:
13
+ // { present, wiped, setStateCount, setStateCountValid, counters:{...}, onHookUpdateInstalled }
14
+ //
15
+ // NaN/undefined-safe: the host may seed setStateCount as null (then react-dom's
16
+ // `null++` yields NaN) or wipe the whole object to undefined after hydration.
17
+ // This reader reports those states honestly instead of throwing.
18
+
19
+ const m = globalThis.__ATL_REACT_METRICS;
20
+
21
+ if (m == null) {
22
+ ctx.log.push('__ATL_REACT_METRICS is ' + String(m) + ' (host absent or wiped post-hydration)');
23
+ return {
24
+ present: false,
25
+ wiped: typeof m === 'undefined',
26
+ note: 'Host metrics object not present. It may not exist yet, or the app wiped it after hydration. Reads return no data; use hook-tracker to own the onHookUpdate slot regardless.',
27
+ };
28
+ }
29
+
30
+ const wantKeys = ctx.args.keys
31
+ ? String(ctx.args.keys).split(',').map((s) => s.trim()).filter(Boolean)
32
+ : null;
33
+ const raw = !!ctx.args.raw;
34
+
35
+ const isNumericCounter = (v) => typeof v === 'number';
36
+
37
+ const counters = {};
38
+ for (const k of Object.keys(m)) {
39
+ if (wantKeys && !wantKeys.includes(k)) continue;
40
+ const v = m[k];
41
+ if (raw) { counters[k] = typeof v === 'function' ? '[Function ' + (v.name || 'anonymous') + ']' : v; continue; }
42
+ if (isNumericCounter(v)) counters[k] = v;
43
+ }
44
+
45
+ const ssc = m.setStateCount;
46
+ const setStateCountValid = typeof ssc === 'number' && Number.isFinite(ssc);
47
+
48
+ ctx.log.push('setStateCount=' + String(ssc) + ' valid=' + setStateCountValid + ' onHookUpdate=' + typeof m.onHookUpdate);
49
+
50
+ return {
51
+ present: true,
52
+ wiped: false,
53
+ setStateCount: ssc,
54
+ setStateCountValid,
55
+ setStateCountNote: setStateCountValid
56
+ ? null
57
+ : 'setStateCount is ' + String(ssc) + '. Host likely seeded it as null/undefined and react-dom incremented it to NaN. It is still monotonic-per-run once numeric; re-seed via hook-tracker install if you need a clean baseline.',
58
+ onHookUpdateInstalled: typeof m.onHookUpdate === 'function',
59
+ counters,
60
+ };
@@ -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/**"]}