@flemo/devtools 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/noop.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  //#region src/anomalies.ts
2
2
  var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
3
- let n = [], { t0Ms: r, driver: i, frameSamples: a, playerGaps: o, longTasks: s, holdLongTasks: c, landing: l } = e, u = r + (e.releasedAtMs ?? 0);
4
- if (o && o.over30Count > 0 && n.push(`player frame gap up to ${o.maxMs}ms ×${o.over30Count} during flight (the rAF player missed frames — convergence jank on this navigation)`), a.released.over30Count > 0) {
3
+ let n = [], { t0Ms: r, driver: i, frameSamples: a, longTasks: o, holdLongTasks: s, landing: c } = e, l = r + (e.releasedAtMs ?? 0);
4
+ if (a.released.over30Count > 0) {
5
5
  let e = i === "compiled" ? " (compiled/compositor flights can still present cleanly through main-thread gaps)" : "";
6
6
  n.push(`main-thread rAF gap up to ${a.released.maxGapMs}ms ×${a.released.over30Count} during visible motion${e}`);
7
7
  }
8
- for (let e of s) e.startMs <= u + 120 && e.startMs + e.durationMs >= u - 50 ? n.push(`long task ${Math.round(e.durationMs)}ms overlapped the visible-motion start (opening-swallow risk: the first presented frames of the transition may have been lost)`) : e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms mid-flight`);
9
- for (let e of c) e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms absorbed by the hold (screen posed, not yet moving — the hold doing its job, not user-visible jank)`);
10
- let { motion: d, images: f } = e;
11
- return d.holdReassertedAtMs !== null && n.push(`hold re-asserted ${d.holdReassertedAtMs}ms into the flight, after it had already released (an interleaved commit wrote the stale paused hold attribute over a running flight — the flemo 2026-08-18 release-race signature; the motion pauses while every timing metric stays clean)`), d.pausedAfterRelease && n.push("compiled animation reported playState=paused after its release (the flight was posed and then stopped mid-motion, not merely starved of frames)"), d.longestStallMs >= 48 && n.push(`motion stalled ${d.longestStallMs}ms mid-flight (${d.stalledFrames}/${d.sampledFrames} released frames advanced neither the animation clock nor the pose — freeze, or freeze-then-leap if the flight still landed on time)`), d.sampledFrames === 0 && e.releasedAtMs !== null && n.push("no released frames were sampled (the flight ended at or before its own hold release — the visible motion, if any, was never observed)"), f.completedUnheld > 0 && n.push(`${f.completedUnheld} image(s) finished loading mid-flight without a hold (${f.completedDuringFlight} completed, ${f.heldDuringFlight} held; ${f.loadingAtStart} were loading at t0, ${f.addedDuringFlight} arrived mid-flight) — each decode rasters on the moving layer and costs a present; this is the warm-side image-hold regression`), l.orphanedHolds.length > 0 && n.push(`hold markers left on the page at rest: ${l.orphanedHolds.join("; ")} (whatever they hide has no owner left to reveal it — the permanently-blank-avatar class)`), l.residualInlineTransforms.length > 0 && n.push(`residual inline style after COMPLETED: ${l.residualInlineTransforms.join("; ")} (landing cleanup failure — the landed scope belongs to the compiled rest rules)`), l.offViewportAtRest && n.push("screen resting at from-pose while COMPLETED+active (blank-viewport signature — the flemo PR #259 class: a residual pose left the landed screen parked off-viewport)"), l.stuckStatuses.length > 0 && n.push(`transitional status stuck >${t / 1e3}s: ${l.stuckStatuses.join(", ")} (navigation queue lock or missed animationend — later navigations will be swallowed)`), i === "unknown" && n.push("driver could not be classified (no running flemo-* CSSAnimation and no player inline-style signature observed — zero-duration flight, or the sampler attached after motion ended)"), n;
8
+ for (let e of o) e.startMs <= l + 120 && e.startMs + e.durationMs >= l - 50 ? n.push(`long task ${Math.round(e.durationMs)}ms overlapped the visible-motion start (opening-swallow risk: the first presented frames of the transition may have been lost)`) : e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms mid-flight`);
9
+ for (let e of s) e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms absorbed by the hold (screen posed, not yet moving — the hold doing its job, not user-visible jank)`);
10
+ let { motion: u, images: d } = e;
11
+ u.holdReassertedAtMs !== null && n.push(`hold re-asserted ${u.holdReassertedAtMs}ms into the flight, after it had already released (an interleaved commit wrote the stale paused hold attribute over a running flight — the flemo 2026-08-18 release-race signature; the motion pauses while every timing metric stays clean)`), u.pausedAfterRelease && n.push("compiled animation reported playState=paused after its release (the flight was posed and then stopped mid-motion, not merely starved of frames)"), u.longestStallMs >= 48 && n.push(`motion stalled ${u.longestStallMs}ms mid-flight (${u.stalledFrames}/${u.sampledFrames} released frames advanced neither the animation clock nor the pose — freeze, or freeze-then-leap if the flight still landed on time)`), u.sampledFrames === 0 && e.releasedAtMs !== null && n.push("no released frames were sampled (the flight ended at or before its own hold release — the visible motion, if any, was never observed)"), d.completedUnheld > 0 && n.push(`${d.completedUnheld} image(s) finished loading mid-flight without a hold (${d.completedDuringFlight} completed, ${d.heldDuringFlight} held; ${d.loadingAtStart} were loading at t0, ${d.addedDuringFlight} arrived mid-flight) — each decode rasters on the moving layer and costs a present; this is the warm-side image-hold regression`), c.orphanedHolds.length > 0 && n.push(`hold markers left on the page at rest: ${c.orphanedHolds.join("; ")} (whatever they hide has no owner left to reveal it — the permanently-blank-avatar class)`), c.residualInlineTransforms.length > 0 && n.push(`residual inline style after COMPLETED: ${c.residualInlineTransforms.join("; ")} (landing cleanup failure — the landed scope belongs to the compiled rest rules)`), c.offViewportAtRest && n.push("screen resting at from-pose while COMPLETED+active (blank-viewport signature — the flemo PR #259 class: a residual pose left the landed screen parked off-viewport)"), c.stuckStatuses.length > 0 && n.push(`transitional status stuck >${t / 1e3}s: ${c.stuckStatuses.join(", ")} (navigation queue lock or missed animationend — later navigations will be swallowed)`);
12
+ let { morphs: f, tripwires: p } = e;
13
+ for (let e of p) n.push(`tripwire ${e.kind} at +${e.atMs}ms: ${e.detail}`);
14
+ return f.skipped.length > 0 && n.push(`shared element(s) did not fly: ${f.skipped.join(", ")} (${f.pairable.length} pairable key(s), ${f.flew.length} flew) — both ends were registered on two different screens and neither was stamped with a flight role, so the pair was never made`), f.duplicatedKeys.length > 0 && n.push(`pairing key(s) used twice inside one screen: ${f.duplicatedKeys.join(", ")} (two ends under one screen are not a pair, so one of them can never fly — this is in the consuming app)`), f.strandedRoles > 0 && n.push(`${f.strandedRoles} morph element(s) still carry a flight role at rest (the stranded participant class: a role that outlives its flight stays in the layer and poisons the NEXT pairing, which is how one interrupted gesture turned into every later pop losing its camera)`), (f.strandedStandIns > 0 || f.strandedGhosts > 0 || f.layerResidue > 0) && n.push(`morph residue at rest: ${f.strandedStandIns} stand-in(s), ${f.strandedGhosts} ghost(s), ${f.layerResidue} element(s) left in a flight layer (a stand-in is a hole in the layout and a layer element is a corpse the next flight will pair against)`), f.leakedSheetRules > 0 && n.push(`${f.leakedSheetRules} morph keyframe rule(s) were left in the per-flight sheet (a flight that never landed keeps its keyframes; they accumulate for the life of the page)`), i === "unknown" && n.push("driver could not be classified (no running flemo-* CSSAnimation and no player inline-style signature observed — zero-duration flight, or the sampler attached after motion ended)"), n;
12
15
  }, s = (e) => {
13
16
  let n = [];
14
17
  if (e.emulationSuspected) {
@@ -21,24 +24,48 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
21
24
  "Display-hardware effects: local dimming, backlight modulation, and panel overdrive alter perceived motion and are invisible to every web API.",
22
25
  "Compositor-internal present skips: a frame can be produced on time (clean rAF, clean player gaps) yet dropped or delayed inside the compositor's present queue; only platform tools (about:tracing, Instruments) see that layer.",
23
26
  "DevTools device emulation composites the page to a rescaled surface: every in-page instrument reads the pre-scale surface and reports clean while the eye watches the post-scale one, so emulated sessions grow shimmer/flash artifacts that do not exist in a plain window or on a real device."
24
- ], l = () => {
27
+ ], l = (e) => {
28
+ /* v8 ignore next -- unreachable: a group exists only because a flight was
29
+ put in it, so nothing ever asks for the median of nothing. */
30
+ if (e.length === 0) return 0;
31
+ let t = [...e].sort((e, t) => e - t);
32
+ return Math.round(t[Math.floor(t.length / 2)] * 10) / 10;
33
+ }, u = (e) => {
34
+ let t = e.filter((e) => typeof e.bucket == "string");
35
+ if (t.length === 0) return [];
36
+ let n = /* @__PURE__ */ new Map();
37
+ for (let e of t) {
38
+ let t = e.bucket, r = n.get(t);
39
+ r ? r.push(e) : n.set(t, [e]);
40
+ }
41
+ return [...n.entries()].map(([e, t]) => ({
42
+ bucket: e,
43
+ flights: t.length,
44
+ medianDurationMs: l(t.map((e) => e.durationMs)),
45
+ medianReleasedGapMs: l(t.map((e) => e.frameSamples.released.medianGapMs)),
46
+ worstReleasedGapMs: t.reduce((e, t) => Math.max(e, t.frameSamples.released.maxGapMs), 0),
47
+ longGapCount: t.reduce((e, t) => e + t.frameSamples.released.over30Count, 0),
48
+ anomalyCount: t.reduce((e, t) => e + t.anomalies.length, 0),
49
+ stalledFlights: t.filter((e) => e.motion.stalledFrames > 0).length
50
+ })).sort((e, t) => e.bucket.localeCompare(t.bucket));
51
+ }, d = () => {
25
52
  if (typeof navigator > "u") return null;
26
53
  let e = navigator.userAgentData;
27
54
  return !e || !Array.isArray(e.brands) ? null : e.brands.map((e) => ({
28
55
  brand: String(e.brand ?? ""),
29
56
  version: String(e.version ?? "")
30
57
  }));
31
- }, u = () => {
58
+ }, f = () => {
32
59
  if (typeof navigator > "u") return "unknown";
33
- let e = l();
60
+ let e = d();
34
61
  if (e?.some((e) => /Chromium/i.test(e.brand))) return "blink";
35
62
  let t = navigator.userAgent ?? "";
36
63
  return /Firefox|FxiOS/i.test(t) ? "gecko" : e || /AppleWebKit/i.test(t) && !/Chrome|Chromium|Android/i.test(t) ? "webkit" : /Android/i.test(t) ? "blink" : "unknown";
37
- }, d = () => {
38
- if (typeof navigator > "u" || u() !== "blink") return !1;
64
+ }, p = () => {
65
+ if (typeof navigator > "u" || f() !== "blink") return !1;
39
66
  let e = navigator.platform ?? "";
40
67
  return /Mac|Win|Linux/i.test(e) ? /Android/i.test(navigator.userAgent ?? "") && !/Mac|Win/i.test(e) ? /Linux (x86_64|i686)/i.test(e) : (navigator.maxTouchPoints ?? 0) > 0 : !1;
41
- }, f = (e = 20) => new Promise((t) => {
68
+ }, m = (e = 20) => new Promise((t) => {
42
69
  let n = Number.isFinite(e) ? Math.max(1, Math.floor(e)) : 20;
43
70
  if (typeof requestAnimationFrame != "function" || typeof performance > "u") {
44
71
  t({
@@ -60,45 +87,68 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
60
87
  requestAnimationFrame(a);
61
88
  };
62
89
  requestAnimationFrame(a);
63
- }), p = () => {
90
+ }), h = [
91
+ "__vite_plugin_react_preamble_installed__",
92
+ "__vite__",
93
+ "__NEXT_HMR_CB",
94
+ "__NEXT_HMR_LATENCY_CB",
95
+ "__webpack_hot_middleware_reporter__",
96
+ "webpackHotUpdate",
97
+ "__REACT_REFRESH_RUNTIME__",
98
+ "$RefreshReg$"
99
+ ], g = () => {
100
+ if (typeof window > "u") return [];
101
+ let e = [];
102
+ for (let t of h) try {
103
+ window[t] !== void 0 && e.push(t);
104
+ } catch {}
105
+ return e;
106
+ }, _ = () => {
64
107
  try {
65
108
  return typeof PerformanceObserver < "u" && Array.isArray(PerformanceObserver.supportedEntryTypes) && PerformanceObserver.supportedEntryTypes.includes("longtask");
66
109
  } catch {
67
110
  return !1;
68
111
  }
69
- }, m = (e) => {
70
- let t = typeof navigator < "u" ? navigator : null, n = typeof window < "u" ? window : null, r = !1;
112
+ }, v = (e, t = !1) => {
113
+ let n = typeof navigator < "u" ? navigator : null, r = typeof window < "u" ? window : null, i = !1;
71
114
  try {
72
- r = n?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? !1;
115
+ i = r?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? !1;
73
116
  } catch {
74
- r = !1;
117
+ i = !1;
75
118
  }
76
119
  return {
77
- userAgent: t?.userAgent ?? "",
78
- uaBrands: l(),
79
- engine: u(),
80
- platform: t?.platform ?? "",
81
- maxTouchPoints: t?.maxTouchPoints ?? 0,
82
- devicePixelRatio: n?.devicePixelRatio ?? 1,
120
+ userAgent: n?.userAgent ?? "",
121
+ uaBrands: d(),
122
+ engine: f(),
123
+ platform: n?.platform ?? "",
124
+ maxTouchPoints: n?.maxTouchPoints ?? 0,
125
+ devicePixelRatio: r?.devicePixelRatio ?? 1,
126
+ hardwareConcurrency: n?.hardwareConcurrency ?? 0,
83
127
  screen: {
84
- width: n?.screen?.width ?? 0,
85
- height: n?.screen?.height ?? 0
128
+ width: r?.screen?.width ?? 0,
129
+ height: r?.screen?.height ?? 0
86
130
  },
87
131
  viewport: {
88
- width: n?.innerWidth ?? 0,
89
- height: n?.innerHeight ?? 0
132
+ width: r?.innerWidth ?? 0,
133
+ height: r?.innerHeight ?? 0
90
134
  },
91
- visualViewportScale: n?.visualViewport?.scale ?? null,
135
+ visualViewportScale: r?.visualViewport?.scale ?? null,
92
136
  rafCadence: e,
93
- reducedMotion: r,
94
- emulationSuspected: d(),
137
+ reducedMotion: i,
138
+ developmentHints: g(),
139
+ emulationSuspected: p(),
95
140
  observation: {
96
- longTasks: p(),
141
+ longTasks: _(),
97
142
  elementAnimations: typeof Element < "u" && typeof Element.prototype.getAnimations == "function",
98
- playerGapMirror: Array.isArray(n?.__flemoPlayerGaps)
143
+ animationEvents: t
99
144
  }
100
145
  };
101
- }, h = "flemo:devtools-panel-height", g = [{
146
+ }, y = [
147
+ "DevTools must be CLOSED while judging motion. An open inspector serializes requests and repaints its own panels on the same machine; the 2026-08 campaign's entire residual 'stutter' was this, and it is invisible to every in-page metric.",
148
+ "No screen recording or display capture while judging. A capture client forces WindowServer to composite every vsync, which SUPPRESSES the symptom — a capture that looks smooth proves nothing about the uncaptured session.",
149
+ "Judge with real input (a finger, a mouse). Synthetic dispatch — evaluate().click() and friends — never fires pointerdown, so it bypasses the swipe/gesture machinery that a real navigation goes through.",
150
+ "Establish the viewing configuration before believing any verdict: device emulation off, which physical display, its refresh rate and HiDPI scaling, Low Power Mode. Ask for a photo of the setup if it is not certain."
151
+ ], b = "flemo:devtools-panel-height", x = [{
102
152
  key: "flemo:devtools",
103
153
  storage: "session",
104
154
  kind: "opt-in-diagnostic",
@@ -106,13 +156,13 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
106
156
  fallback: "off",
107
157
  effect: "arms this flight recorder in the playground (?devtools=on)"
108
158
  }, {
109
- key: h,
159
+ key: b,
110
160
  storage: "session",
111
161
  kind: "production-state",
112
162
  values: "a pixel height",
113
163
  fallback: "(the panel's default height)",
114
164
  effect: "the drawer height this panel was last dragged to"
115
- }], _ = [], v = [..._, ...g], y = [
165
+ }], S = [], C = [...S, ...x], w = [
116
166
  {
117
167
  key: "flemo:sixty",
118
168
  storage: "session",
@@ -233,45 +283,237 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
233
283
  storage: "session",
234
284
  retiredWith: "the Low Power Mode release-latency probe (2026-08-22)"
235
285
  }
236
- ], b = "(retired — the library no longer reads this)", x = (e) => {
286
+ ], T = "(retired — the library no longer reads this)", E = (e) => {
237
287
  try {
238
288
  let t = e === "session" ? sessionStorage : localStorage;
239
289
  return t.length, t;
240
290
  } catch {
241
291
  return null;
242
292
  }
243
- }, S = (e, t) => {
293
+ }, D = (e, t) => {
244
294
  try {
245
295
  return e ? e.getItem(t) : null;
246
296
  } catch {
247
297
  return null;
248
298
  }
249
- }, C = new Set(v.map((e) => e.key)), w = new Set(y.map((e) => e.key)), T = () => {
250
- let e = {}, t = x("session"), n = x("local");
251
- for (let r of v) {
252
- let i = S(r.storage === "session" ? t : n, r.key);
299
+ }, ee = new Set(C.map((e) => e.key)), te = new Set(w.map((e) => e.key)), ne = () => {
300
+ let e = {}, t = E("session"), n = E("local");
301
+ for (let r of C) {
302
+ let i = D(r.storage === "session" ? t : n, r.key);
253
303
  i !== null && (e[r.key] = i);
254
304
  }
255
- for (let r of y) for (let [i, a] of [[t, "sessionStorage"], [n, "localStorage"]]) {
256
- let t = S(i, r.key);
257
- t !== null && (e[`${r.key} (${a}) ${b}`] = t);
305
+ for (let r of w) for (let [i, a] of [[t, "sessionStorage"], [n, "localStorage"]]) {
306
+ let t = D(i, r.key);
307
+ t !== null && (e[`${r.key} (${a}) ${T}`] = t);
258
308
  }
259
309
  for (let [r, i] of [[t, "sessionStorage"], [n, "localStorage"]]) if (r) try {
260
310
  for (let t = 0; t < r.length; t += 1) {
261
311
  let n = r.key(t);
262
- !n || !n.startsWith("flemo:") || C.has(n) || w.has(n) || (e[`${n} (${i}, unknown key)`] = r.getItem(n) ?? "");
312
+ !n || !n.startsWith("flemo:") || ee.has(n) || te.has(n) || (e[`${n} (${i}, unknown key)`] = r.getItem(n) ?? "");
263
313
  }
264
314
  } catch {}
265
315
  return e;
266
- }, E = (e) => {
316
+ }, O = (e) => {
267
317
  let t = [];
268
318
  for (let [n, r] of Object.entries(e)) if (n.includes("(retired — the library no longer reads this)")) {
269
- let e = n.slice(0, n.indexOf(" (")), i = y.find((t) => t.key === e);
319
+ let e = n.slice(0, n.indexOf(" (")), i = w.find((t) => t.key === e);
270
320
  t.push(`${n}=${r} — RETIRED residue: this key went with ${i?.retiredWith ?? "a removed feature"}. The library never reads it, so it cannot explain anything you are seeing. Safe to delete; it is listed only so it does not read as an unexplained lead.`);
271
321
  continue;
272
322
  }
273
323
  return t;
274
- }, D = (e) => Math.round(e * 10) / 10, O = (e) => {
324
+ }, k = "flemo:devtools-trace", A = 10, j = 64e3, M = () => {
325
+ try {
326
+ let e = sessionStorage;
327
+ return e.length, e;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }, N = (e, t) => {
332
+ let n = M();
333
+ if (!n) return;
334
+ let r = e.slice(-10);
335
+ for (; r.length > 0;) {
336
+ let e = JSON.stringify({
337
+ version: t,
338
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
339
+ flights: r
340
+ });
341
+ if (e.length <= 64e3) {
342
+ try {
343
+ n.setItem(k, e);
344
+ } catch {}
345
+ return;
346
+ }
347
+ r = r.slice(1);
348
+ }
349
+ P();
350
+ }, P = () => {
351
+ try {
352
+ M()?.removeItem(k);
353
+ } catch {}
354
+ }, F = (e) => {
355
+ let t = M();
356
+ if (!t) return null;
357
+ let n = null;
358
+ try {
359
+ n = t.getItem(k);
360
+ } catch {
361
+ return null;
362
+ }
363
+ if (n === null) return null;
364
+ let r = null;
365
+ try {
366
+ r = JSON.parse(n);
367
+ } catch {
368
+ return P(), null;
369
+ }
370
+ return !r || r.version !== e || !Array.isArray(r.flights) ? (P(), null) : {
371
+ savedAt: typeof r.savedAt == "string" ? r.savedAt : "",
372
+ flights: r.flights,
373
+ note: "These flights were recorded by the page instance BEFORE the last full load. They are kept apart from the live ones because they may have come from a different build."
374
+ };
375
+ }, I = 12, L = 20, R = 40, z = 500, B = 200, V = (e) => {
376
+ let t = e.rafCadence.medianGapMs;
377
+ if (t === null) return {
378
+ id: "display-cadence",
379
+ status: "unknown",
380
+ detail: "the idle frame cadence could not be sampled, so the display's rate is unknown"
381
+ };
382
+ let n = { medianIdleFrameMs: t };
383
+ return t < I ? {
384
+ id: "display-cadence",
385
+ status: "ok",
386
+ detail: `idle frames arrive every ${t}ms — a display above 60Hz. Frame budgets in this report are still quoted against 60Hz (a gap of 30ms or more)`,
387
+ metrics: n
388
+ } : t <= L ? {
389
+ id: "display-cadence",
390
+ status: "ok",
391
+ detail: `idle frames arrive every ${t}ms — a 60Hz clock`,
392
+ metrics: n
393
+ } : t <= R ? {
394
+ id: "display-cadence",
395
+ status: "violated",
396
+ detail: `idle frames arrive every ${t}ms — the page is being served roughly HALF rate. On iOS this is Low Power Mode, whose main-thread ceiling is ~30Hz while the compositor keeps 60: motion judged here is judged against a clock no transition can beat, and the ceiling is not a library defect. Turn it off, or judge elsewhere`,
397
+ metrics: n
398
+ } : {
399
+ id: "display-cadence",
400
+ status: "violated",
401
+ detail: `idle frames arrive every ${t}ms — the page is barely being served frames at all while idle. Something outside the transition owns this machine; no motion verdict taken here means anything`,
402
+ metrics: n
403
+ };
404
+ }, H = (e) => {
405
+ if (!e.environment.observation.longTasks) return {
406
+ id: "machine-idle",
407
+ status: "unknown",
408
+ detail: "this browser does not report long tasks, so the recorder cannot tell whether anything else was competing for the main thread"
409
+ };
410
+ let t = e.idleLongTasks.length, n = Math.round(e.idleLongTasks.reduce((e, t) => e + t.durationMs, 0)), r = Math.round(e.idleLongTasks.reduce((e, t) => Math.max(e, t.durationMs), 0)), i = {
411
+ idleLongTasks: t,
412
+ idleLongTaskMs: n,
413
+ worstIdleTaskMs: r
414
+ };
415
+ return n >= z || r >= B ? {
416
+ id: "machine-idle",
417
+ status: "violated",
418
+ detail: `${t} long task(s) totalling ${n}ms ran while NO navigation was in flight (worst ${r}ms). Something else on this page or this machine was busy, and a motion verdict taken under someone else's load measures the load. A whole tremble campaign was invalidated by exactly this, with a build running in another window`,
419
+ metrics: i
420
+ } : {
421
+ id: "machine-idle",
422
+ status: "ok",
423
+ detail: `${t} idle long task(s), ${n}ms total — the page was quiet between navigations`,
424
+ metrics: i
425
+ };
426
+ }, U = (e) => {
427
+ let { trusted: t, synthetic: n, pointerTypes: r } = e.input;
428
+ return n > 0 ? {
429
+ id: "real-input",
430
+ status: "violated",
431
+ detail: `${n} script-dispatched (untrusted) input event(s) drove this session. Synthetic dispatch never fires the gesture machinery a real navigation goes through, so a clean run proves nothing about a finger`,
432
+ metrics: {
433
+ trusted: t,
434
+ synthetic: n
435
+ }
436
+ } : t === 0 ? {
437
+ id: "real-input",
438
+ status: "unknown",
439
+ detail: "no input event preceded any recorded flight. These navigations were driven programmatically or by the browser's own back/forward, which is a different path from the one a user takes",
440
+ metrics: {
441
+ trusted: t,
442
+ synthetic: n
443
+ }
444
+ } : {
445
+ id: "real-input",
446
+ status: "ok",
447
+ detail: `${t} trusted input event(s) drove this session (${r.join(", ") || "type unreported"})`,
448
+ metrics: {
449
+ trusted: t,
450
+ synthetic: n
451
+ }
452
+ };
453
+ }, re = (e) => e.input.pointerTypes.includes("touch") ? {
454
+ id: "touch-path",
455
+ status: "ok",
456
+ detail: "at least one navigation was driven by touch"
457
+ } : e.environment.maxTouchPoints === 0 ? {
458
+ id: "touch-path",
459
+ status: "unknown",
460
+ detail: "this device reports no touch points, so there is no touch path to exercise here"
461
+ } : {
462
+ id: "touch-path",
463
+ status: "unknown",
464
+ detail: "no touch-driven navigation was recorded on a touch-capable device. The swipe and pointer-capture paths are only reached by a finger; a build that had broken them outright once passed every automated layer green because every probe used a mouse"
465
+ }, W = [
466
+ {
467
+ id: "devtools-closed",
468
+ status: "unknown",
469
+ detail: "whether an inspector is open cannot be seen from inside the page. An open one serializes requests and repaints its own panels on the same machine; a whole campaign's residual stutter was this and nothing else. Confirm it with the user"
470
+ },
471
+ {
472
+ id: "no-screen-capture",
473
+ status: "unknown",
474
+ detail: "whether the screen is being captured cannot be seen from inside the page. A capture client forces the compositor to present every vsync, which SUPPRESSES the symptom — a recording that looks smooth proves nothing about the uncaptured session"
475
+ },
476
+ {
477
+ id: "viewing-configuration",
478
+ status: "unknown",
479
+ detail: "which physical display this window is on, its refresh rate and scaling, cannot be read from the page. Establish it with the user before believing any verdict"
480
+ }
481
+ ], G = (e) => {
482
+ let { environment: t } = e, n = [];
483
+ return n.push(t.emulationSuspected ? {
484
+ id: "device-emulation",
485
+ status: "violated",
486
+ detail: "DevTools device emulation is suspected (Blink on a desktop platform reporting touch points). Emulation composites the page to a rescaled surface: every in-page instrument reads the pre-scale surface and reports clean while the eye watches the post-scale one" + (/Win/i.test(t.platform) ? ". Windows touch hardware makes this signal ambiguous — confirm the device toolbar state" : "")
487
+ } : {
488
+ id: "device-emulation",
489
+ status: "ok",
490
+ detail: "no device-emulation signature on this session"
491
+ }), n.push(V(t)), n.push(e.documentHidden || e.wentHidden ? {
492
+ id: "page-foreground",
493
+ status: "violated",
494
+ detail: e.documentHidden ? "this document is HIDDEN right now — a backgrounded page is throttled to a fraction of its frames" : "this document was hidden at least once while recording; any flight that ran then was throttled"
495
+ } : {
496
+ id: "page-foreground",
497
+ status: "ok",
498
+ detail: "the document stayed visible throughout"
499
+ }), n.push(H(e)), n.push(t.developmentHints.length > 0 ? {
500
+ id: "build-mode",
501
+ status: "violated",
502
+ detail: `development-server globals are present (${t.developmentHints.join(", ")}). A development build is a different program: unminified, double-invoking, and hot-reload instrumented. A day of ladder measurements once turned out to be measuring the build itself`
503
+ } : {
504
+ id: "build-mode",
505
+ status: "unknown",
506
+ detail: "no development-server global was found. That is consistent with a production build but does not prove one — confirm how this page was built"
507
+ }), n.push(U(e)), n.push(re(e)), n.push(t.reducedMotion ? {
508
+ id: "reduced-motion",
509
+ status: "violated",
510
+ detail: "this session prefers reduced motion, so the transitions under test are being suppressed or shortened. Whatever is on screen is not the motion being judged"
511
+ } : {
512
+ id: "reduced-motion",
513
+ status: "ok",
514
+ detail: "reduced motion is not requested"
515
+ }), [...n, ...W];
516
+ }, K = (e) => Math.round(e * 10) / 10, q = (e) => {
275
517
  if (e.length === 0) return {
276
518
  count: 0,
277
519
  medianGapMs: 0,
@@ -281,11 +523,11 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
281
523
  let t = [...e].sort((e, t) => e - t);
282
524
  return {
283
525
  count: e.length,
284
- medianGapMs: D(t[Math.floor(t.length / 2)]),
285
- maxGapMs: D(t[t.length - 1]),
526
+ medianGapMs: K(t[Math.floor(t.length / 2)]),
527
+ maxGapMs: K(t[t.length - 1]),
286
528
  over30Count: e.filter((e) => e >= 30).length
287
529
  };
288
- }, k = (e, t = []) => {
530
+ }, J = (e, t = []) => {
289
531
  let n = [...e, ...t];
290
532
  return {
291
533
  ...n.length === 0 ? {
@@ -296,21 +538,18 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
296
538
  let e = [...n].sort((e, t) => e - t);
297
539
  return {
298
540
  count: n.length,
299
- medianGapMs: D(e[Math.floor(e.length / 2)]),
300
- maxGapMs: D(e[e.length - 1])
541
+ medianGapMs: K(e[Math.floor(e.length / 2)]),
542
+ maxGapMs: K(e[e.length - 1])
301
543
  };
302
544
  })(),
303
- longGaps: n.filter((e) => e >= 30).map(D),
304
- held: O(e),
305
- released: O(t)
545
+ longGaps: n.filter((e) => e >= 30).map(K),
546
+ held: q(e),
547
+ released: q(t)
306
548
  };
307
- }, A = (e) => e.length === 0 ? null : {
308
- maxMs: D(Math.max(...e)),
309
- over30Count: e.filter((e) => e >= 30).length
310
- }, j = (e) => {
311
- let t = e.playerSuppression || e.playerAdvance;
312
- return t && e.compiledAnimation ? "mixed" : t ? "player" : e.compiledAnimation ? "compiled" : "unknown";
313
- }, M = (e) => e === "PUSHING" ? "PUSH" : e === "POPPING" ? "POP" : e === "REPLACING" ? "REPLACE" : null, N = (e) => {
549
+ }, Y = (e) => {
550
+ let t = e.inlineSuppression || e.inlineAdvance;
551
+ return t && e.compiledAnimation ? "mixed" : t ? "inline" : e.compiledAnimation ? "compiled" : "unknown";
552
+ }, X = (e) => e === "PUSHING" ? "PUSH" : e === "POPPING" ? "POP" : e === "REPLACING" ? "REPLACE" : null, ie = (e) => {
314
553
  let t = /^matrix3d\(([^)]+)\)$/.exec(e.trim());
315
554
  if (t) {
316
555
  let e = t[1].split(",").map((e) => Number(e.trim()));
@@ -322,9 +561,37 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
322
561
  return e.length === 6 && Number.isFinite(e[4]) ? e[4] : null;
323
562
  }
324
563
  return null;
325
- }, P = "2", F = "@flemo/devtools resolved to its production entry: nothing was recorded. Run a development build to collect a real report.", I = () => {}, L = () => ({
564
+ }, ae = (e) => {
565
+ /* v8 ignore next -- unreachable: deriveVerdict returns before this on an
566
+ empty flight list, so nothing ever asks for the median of nothing. */
567
+ if (e.length === 0) return 0;
568
+ let t = [...e].sort((e, t) => e - t);
569
+ return t[Math.floor(t.length / 2)];
570
+ }, oe = (e) => {
571
+ let t = [], n = e.preconditions.filter((e) => e.status === "violated");
572
+ if (n.length > 0 && t.push(`NOT EVIDENCE: ${n.length} judging precondition(s) failed — ${n.map((e) => e.id).join(", ")}. Everything below was measured under those conditions; fix them and measure again before drawing a conclusion from it.`), e.flights.length === 0) return t.push("No flights were recorded. Either no navigation happened while the recorder was attached, or it attached after the one you meant to measure — the recorder only sees flights that start after it does."), t;
573
+ e.observation.animationEvents || t.push("The animation channel observed NOTHING across this whole session: not one flemo keyframe reported a start, an end or a cancel, though flights were recorded. Treat every animation-derived field here as unmeasured rather than as clean — a probe that never fires reads exactly like a page with nothing to report.");
574
+ let r = e.flights, i = r.reduce((e, t) => Math.max(e, t.frameSamples.released.maxGapMs), 0), a = r.filter((e) => e.anomalies.length > 0).length;
575
+ t.push(`${r.length} flight(s) recorded. Median duration ${ae(r.map((e) => e.durationMs))}ms, worst frame gap during visible motion ${i}ms, ${a} flight(s) carrying at least one anomaly.`);
576
+ let o = r.filter((e) => e.motion.longestStallMs >= 48);
577
+ o.length > 0 && t.push(`${o.length} flight(s) STOPPED MOVING mid-flight (longest ${Math.max(...o.map((e) => e.motion.longestStallMs))}ms). Frames kept arriving; the picture did not change. Look at the hold and the animation's play state, not at the frame budget.`);
578
+ let s = r.filter((e) => e.morphs.skipped.length > 0);
579
+ if (s.length > 0) {
580
+ let e = [...new Set(s.flatMap((e) => e.morphs.skipped))];
581
+ t.push(`Shared elements did NOT fly on ${s.length} flight(s): ${e.join(", ")}. Both ends were registered on two different screens and neither was ever stamped with a flight role, so the pair was never made. This is silent by nature — nothing else on the page says it happened.`);
582
+ }
583
+ let c = [...new Set(r.flatMap((e) => e.morphs.duplicatedKeys))];
584
+ c.length > 0 && t.push(`Pairing key(s) used twice inside one screen: ${c.join(", ")}. Two ends under one screen are not a pair, so one of them can never fly. This is in the consuming app, not in the library.`);
585
+ let l = r.flatMap((e) => e.tripwires);
586
+ if (l.length > 0) {
587
+ let e = [...new Set(l.map((e) => e.kind))];
588
+ t.push(`${l.length} tripwire hit(s) across the session (${e.join(", ")}). These are one-frame events reported by the browser itself, not sampled — read the flights' \`tripwires\` arrays for the exact animations.`);
589
+ }
590
+ return r.filter((e) => e.frameSamples.released.over30Count > 0).length === 0 && a === 0 && n.length === 0 && t.push("No flight dropped a frame during visible motion (nothing at or over 30ms) and no anomaly fired. Under the preconditions above, this session is clean — what remains is in `blindSpots`, and none of it is reachable from page code."), t;
591
+ }, se = "3", Z = "@flemo/devtools resolved to its production entry: nothing was recorded. Run a development build to collect a real report.", Q = () => {}, ce = () => ({
326
592
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
327
593
  version: "inert",
594
+ verdict: [Z],
328
595
  environment: {
329
596
  userAgent: "",
330
597
  uaBrands: null,
@@ -345,25 +612,31 @@ var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
345
612
  medianGapMs: null,
346
613
  sampleCount: 0
347
614
  },
615
+ hardwareConcurrency: 0,
348
616
  reducedMotion: !1,
617
+ developmentHints: [],
349
618
  emulationSuspected: !1,
350
619
  observation: {
351
620
  longTasks: !1,
352
621
  elementAnimations: !1,
353
- playerGapMirror: !1
622
+ animationEvents: !1
354
623
  }
355
624
  },
625
+ preconditions: [],
356
626
  overrides: {
357
627
  active: {},
358
- warnings: [F]
628
+ warnings: [Z]
359
629
  },
360
630
  flights: [],
361
- anomalies: [F],
631
+ comparison: [],
632
+ previousSession: null,
633
+ anomalies: [Z],
362
634
  blindSpots: [],
363
- judgingProtocol: [F]
364
- }), R = (e) => ({
365
- detach: I,
366
- report: L
367
- }), z = (e) => ({ detach: I });
635
+ judgingProtocol: [Z]
636
+ }), le = (e) => ({
637
+ detach: Q,
638
+ report: ce,
639
+ mark: () => null
640
+ }), $ = (e) => ({ detach: Q }), ue = (e) => ({ detach: Q });
368
641
  //#endregion
369
- export { c as BLIND_SPOTS, _ as CORE_FLAGS, g as DEVTOOLS_OWNED_FLAGS, v as FLAG_REGISTRY, e as LONG_GAP_MS, i as MID_FLIGHT_TASK_MS, n as OPENING_WINDOW_LEAD_MS, r as OPENING_WINDOW_TAIL_MS, h as PANEL_HEIGHT_KEY, P as REPORT_SCHEMA_VERSION, y as RETIRED_FLAGS, b as RETIRED_MARKER, a as STALL_MS, t as STUCK_STATUS_MS, z as attachDevtoolsPanel, R as attachFlightRecorder, m as captureEnvironment, j as classifyDriver, k as computeFrameStats, O as computePhaseStats, A as computePlayerGapStats, o as deriveFlightAnomalies, E as deriveOverrideWarnings, s as deriveReportAnomalies, u as detectEngine, d as isEmulationSuspected, M as kindFromStatus, N as parseTranslateX, f as sampleRafCadence, T as snapshotOverrides };
642
+ export { c as BLIND_SPOTS, S as CORE_FLAGS, x as DEVTOOLS_OWNED_FLAGS, C as FLAG_REGISTRY, y as JUDGING_PROTOCOL, e as LONG_GAP_MS, j as MAX_PERSISTED_BYTES, A as MAX_PERSISTED_FLIGHTS, i as MID_FLIGHT_TASK_MS, n as OPENING_WINDOW_LEAD_MS, r as OPENING_WINDOW_TAIL_MS, b as PANEL_HEIGHT_KEY, se as REPORT_SCHEMA_VERSION, w as RETIRED_FLAGS, T as RETIRED_MARKER, a as STALL_MS, t as STUCK_STATUS_MS, k as TRACE_KEY, ue as attachDevtoolsHud, $ as attachDevtoolsPanel, le as attachFlightRecorder, v as captureEnvironment, Y as classifyDriver, P as clearTrace, J as computeFrameStats, q as computePhaseStats, o as deriveFlightAnomalies, O as deriveOverrideWarnings, G as derivePreconditions, s as deriveReportAnomalies, oe as deriveVerdict, f as detectEngine, g as developmentHints, p as isEmulationSuspected, X as kindFromStatus, F as loadTrace, ie as parseTranslateX, m as sampleRafCadence, N as saveTrace, ne as snapshotOverrides, u as summariseBuckets };
@@ -10,6 +10,8 @@ export interface DevtoolsPanelOptions {
10
10
  initialOpen?: boolean;
11
11
  /** Corner for the toggle button. Default "bottom-right". */
12
12
  position?: "bottom-right" | "bottom-left";
13
+ /** Labels the A/B button cycles through. Default ["A", "B"]. */
14
+ buckets?: string[];
13
15
  }
14
16
  export interface DevtoolsPanelHandle {
15
17
  detach: () => void;
@@ -1 +1 @@
1
- export declare const PANEL_CSS = "\n:host {\n all: initial;\n color-scheme: dark;\n}\n* { box-sizing: border-box; }\n.root {\n --bg: #14161a;\n --bg-soft: #1b1e24;\n --line: #2b3038;\n --fg: #e6e9ef;\n --fg-dim: #99a1b0;\n --accent: #7cc4ff;\n --warn: #ffb454;\n --bad: #ff6b6b;\n font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: var(--fg);\n}\n@media (prefers-color-scheme: light) {\n .root {\n --bg: #ffffff;\n --bg-soft: #f4f6f9;\n --line: #dde1e8;\n --fg: #1a1d22;\n --fg-dim: #5c6472;\n --accent: #0b6bcb;\n --warn: #a35a00;\n --bad: #c2261f;\n }\n}\n.toggle {\n position: fixed;\n bottom: 12px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 6px 10px;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--bg);\n color: var(--fg);\n font: inherit;\n cursor: pointer;\n}\n.toggle:hover { background: var(--bg-soft); }\n.toggle[data-corner=\"bottom-right\"] { right: 12px; }\n.toggle[data-corner=\"bottom-left\"] { left: 12px; }\n.mark { font-weight: 700; letter-spacing: 0.04em; }\n.count {\n padding: 0 6px;\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n}\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--bad);\n}\n.dot[hidden] { display: none; }\n.panel {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2147482999;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n border-top: 1px solid var(--line);\n background: var(--bg);\n overflow: hidden;\n}\n.panel[hidden] { display: none; }\n.grip {\n flex: 0 0 6px;\n height: 6px;\n background: var(--line);\n cursor: ns-resize;\n touch-action: none;\n}\n.head {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--line);\n}\n.env { color: var(--fg-dim); }\n.spacer { flex: 1 1 auto; }\n.act {\n padding: 4px 8px;\n border: 1px solid var(--line);\n border-radius: 4px;\n background: var(--bg-soft);\n color: var(--fg);\n font: inherit;\n cursor: pointer;\n}\n.chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n flex: 1 1 100%;\n}\n.chips:empty { display: none; }\n.chip {\n padding: 2px 8px;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n max-width: 100%;\n overflow-wrap: anywhere;\n}\n.chip.warn { border-color: var(--warn); color: var(--warn); }\n.chip.bad { border-color: var(--bad); color: var(--bad); }\n.body {\n display: flex;\n flex: 1 1 auto;\n min-height: 0;\n}\n.list {\n flex: 0 0 40%;\n max-width: 340px;\n min-width: 180px;\n overflow: auto;\n border-right: 1px solid var(--line);\n}\n.row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 10px;\n border: 0;\n border-bottom: 1px solid var(--line);\n background: none;\n color: var(--fg);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.row[aria-selected=\"true\"] { background: var(--bg-soft); }\n.row .kind { font-weight: 700; }\n.row .driver { color: var(--accent); }\n.row .dur, .row .screens { color: var(--fg-dim); }\n.row .n {\n margin-left: auto;\n padding: 0 6px;\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n}\n.row .n.bad { background: var(--bad); color: #fff; }\n.detail { flex: 1 1 auto; overflow: auto; padding: 8px 10px; }\n.section { margin: 0 0 10px; }\n.section > h2 {\n margin: 0 0 4px;\n font: inherit;\n font-weight: 700;\n color: var(--fg-dim);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n.kv { display: flex; gap: 8px; }\n.kv > .k { flex: 0 0 34%; max-width: 180px; color: var(--fg-dim); }\n.kv > .v { flex: 1 1 auto; overflow-wrap: anywhere; }\n/* A value that IS the finding \u2014 a stall, a re-asserted hold, an orphan. */\n.kv > .v.bad { color: var(--bad); }\n.li { overflow-wrap: anywhere; }\n.li.bad { color: var(--bad); }\n.dim { color: var(--fg-dim); }\n.spark { display: block; width: 100%; height: 28px; }\n.foot { border-top: 1px solid var(--line); padding: 6px 10px; color: var(--fg-dim); }\n.foot summary { cursor: pointer; }\n.foot .li { margin-top: 4px; }\n";
1
+ export declare const PANEL_CSS = "\n:host {\n all: initial;\n color-scheme: dark;\n}\n* { box-sizing: border-box; }\n.root {\n --bg: #14161a;\n --bg-soft: #1b1e24;\n --line: #2b3038;\n --fg: #e6e9ef;\n --fg-dim: #99a1b0;\n --accent: #7cc4ff;\n --warn: #ffb454;\n --bad: #ff6b6b;\n font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: var(--fg);\n}\n@media (prefers-color-scheme: light) {\n .root {\n --bg: #ffffff;\n --bg-soft: #f4f6f9;\n --line: #dde1e8;\n --fg: #1a1d22;\n --fg-dim: #5c6472;\n --accent: #0b6bcb;\n --warn: #a35a00;\n --bad: #c2261f;\n }\n}\n.toggle {\n position: fixed;\n bottom: 12px;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 6px 10px;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--bg);\n color: var(--fg);\n font: inherit;\n cursor: pointer;\n}\n.toggle:hover { background: var(--bg-soft); }\n.toggle[data-corner=\"bottom-right\"] { right: 12px; }\n.toggle[data-corner=\"bottom-left\"] { left: 12px; }\n.mark { font-weight: 700; letter-spacing: 0.04em; }\n.count {\n padding: 0 6px;\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n}\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--bad);\n}\n.dot[hidden] { display: none; }\n.panel {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2147482999;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n border-top: 1px solid var(--line);\n background: var(--bg);\n overflow: hidden;\n}\n.panel[hidden] { display: none; }\n.grip {\n flex: 0 0 6px;\n height: 6px;\n background: var(--line);\n cursor: ns-resize;\n touch-action: none;\n}\n.head {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--line);\n}\n.env { color: var(--fg-dim); }\n.spacer { flex: 1 1 auto; }\n.act {\n padding: 4px 8px;\n border: 1px solid var(--line);\n border-radius: 4px;\n background: var(--bg-soft);\n color: var(--fg);\n font: inherit;\n cursor: pointer;\n}\n.chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n flex: 1 1 100%;\n}\n.chips:empty { display: none; }\n.chip {\n padding: 2px 8px;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n max-width: 100%;\n overflow-wrap: anywhere;\n}\n/* The verdict reads as prose, not as a tag: it is a sentence about the whole\n session and it is the first thing anybody should read. */\n.chip.lead {\n border-color: var(--fg);\n color: var(--fg);\n white-space: normal;\n max-width: 100%;\n line-height: 1.5;\n}\n.chip.warn { border-color: var(--warn); color: var(--warn); }\n.chip.bad { border-color: var(--bad); color: var(--bad); }\n.body {\n display: flex;\n flex: 1 1 auto;\n min-height: 0;\n}\n.list {\n flex: 0 0 40%;\n max-width: 340px;\n min-width: 180px;\n overflow: auto;\n border-right: 1px solid var(--line);\n}\n.row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 10px;\n border: 0;\n border-bottom: 1px solid var(--line);\n background: none;\n color: var(--fg);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.row[aria-selected=\"true\"] { background: var(--bg-soft); }\n.row .kind { font-weight: 700; }\n.row .driver { color: var(--accent); }\n.row .dur, .row .screens { color: var(--fg-dim); }\n.row .n {\n margin-left: auto;\n padding: 0 6px;\n border-radius: 999px;\n background: var(--bg-soft);\n color: var(--fg-dim);\n}\n.row .n.bad { background: var(--bad); color: #fff; }\n.detail { flex: 1 1 auto; overflow: auto; padding: 8px 10px; }\n.section { margin: 0 0 10px; }\n.section > h2 {\n margin: 0 0 4px;\n font: inherit;\n font-weight: 700;\n color: var(--fg-dim);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n.kv { display: flex; gap: 8px; }\n.kv > .k { flex: 0 0 34%; max-width: 180px; color: var(--fg-dim); }\n.kv > .v { flex: 1 1 auto; overflow-wrap: anywhere; }\n/* A value that IS the finding \u2014 a stall, a re-asserted hold, an orphan. */\n.kv > .v.bad { color: var(--bad); }\n.li { overflow-wrap: anywhere; }\n.li.bad { color: var(--bad); }\n.dim { color: var(--fg-dim); }\n.spark { display: block; width: 100%; height: 28px; }\n.foot { border-top: 1px solid var(--line); padding: 6px 10px; color: var(--fg-dim); }\n.foot summary { cursor: pointer; }\n.foot .li { margin-top: 4px; }\n";
@@ -0,0 +1,20 @@
1
+ import { FlightRecord, PreviousSession } from './types';
2
+ export declare const TRACE_KEY = "flemo:devtools-trace";
3
+ /** Flights carried across a load. The recent ones are the ones anybody reads. */
4
+ export declare const MAX_PERSISTED_FLIGHTS = 10;
5
+ /** Payload ceiling. sessionStorage quotas are small and shared with the app. */
6
+ export declare const MAX_PERSISTED_BYTES = 64000;
7
+ /**
8
+ * Write the tail of the buffer. Trims by count first and then by size, because
9
+ * one flight with a hundred long gaps can be larger than nine ordinary ones.
10
+ */
11
+ export declare const saveTrace: (flights: readonly FlightRecord[], version: string) => void;
12
+ export declare const clearTrace: () => void;
13
+ /**
14
+ * Read the previous page instance's tail, once, at attach.
15
+ *
16
+ * A trace whose schema version does not match this build is DROPPED rather
17
+ * than coerced: a report that mixes two schemas is worse than one that says
18
+ * the older flights are gone.
19
+ */
20
+ export declare const loadTrace: (version: string) => PreviousSession | null;
@@ -0,0 +1,14 @@
1
+ import { EnvironmentFingerprint, InputEvidence, LongTaskSpan, Precondition } from './types';
2
+ export interface PreconditionInput {
3
+ environment: EnvironmentFingerprint;
4
+ /** Long tasks observed while NO flight was open. */
5
+ idleLongTasks: LongTaskSpan[];
6
+ /** How long the recorder has been attached. */
7
+ observedMs: number;
8
+ /** The document was hidden at least once while recording. */
9
+ wentHidden: boolean;
10
+ documentHidden: boolean;
11
+ /** Input observed across every recorded flight. */
12
+ input: InputEvidence;
13
+ }
14
+ export declare const derivePreconditions: (input: PreconditionInput) => Precondition[];