@flemo/devtools 0.2.2 → 0.4.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/README.md +77 -302
- package/dist/__tests__/domProtocol.test.d.ts +1 -0
- package/dist/anomalies.d.ts +0 -4
- package/dist/domProtocol.d.ts +22 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +386 -380
- package/dist/noop.d.ts +1 -1
- package/dist/noop.mjs +122 -103
- package/dist/overrides.d.ts +44 -7
- package/dist/types.d.ts +0 -14
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,69 +1,36 @@
|
|
|
1
1
|
# @flemo/devtools
|
|
2
2
|
|
|
3
|
-
Zero-
|
|
3
|
+
Zero-dependency flight recorder and optional visual panel for [flemo](https://flemo.dev) transitions. It observes existing `data-flemo-*` surfaces, `window.__flemoPlayerGaps`, leftover `flemo:*` keys, `MutationObserver`, `PerformanceObserver("longtask")`, and rAF. It imports neither `@flemo/core` nor `@flemo/react` and does not alter measured motion.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`window.__flemoPlayerGaps`, the `flemo:*` storage registry — plus standard
|
|
7
|
-
observers (`MutationObserver`, `PerformanceObserver("longtask")`, rAF), and
|
|
8
|
-
condenses every navigation into one structured JSON report. The goal: a coding
|
|
9
|
-
agent can diagnose a transition problem from the report alone, without
|
|
10
|
-
round-tripping through the user ("which browser? was DevTools open? did you
|
|
11
|
-
leave a toggle set?").
|
|
5
|
+
## Quickstart
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
```ts
|
|
8
|
+
import { attachFlightRecorder } from "@flemo/devtools";
|
|
9
|
+
|
|
10
|
+
const recorder = attachFlightRecorder({ log: true });
|
|
11
|
+
// ...navigate...
|
|
12
|
+
const report = recorder.report(); // JSON-serializable FlemoReport
|
|
13
|
+
recorder.detach();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Unless `installGlobal: false` or the name is already owned, the recorder installs `window.flemo`:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
copy(JSON.stringify(window.flemo.report(), null, 2));
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `/playground?devtools=on` to enable the playground recorder. `flemo:devtools` persists in `sessionStorage`; `?devtools=off` disables it. `attachFlightRecorder()` is idempotent while attached and returns an inert handle during SSR.
|
|
15
23
|
|
|
16
24
|
## Production safety
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
production, so the ordinary import is the safe one:
|
|
26
|
+
Normal imports use `development` and `production` export conditions. The production implementation is inert and records nothing:
|
|
20
27
|
|
|
21
28
|
```ts
|
|
22
29
|
import { attachFlightRecorder } from "@flemo/devtools";
|
|
23
|
-
|
|
24
|
-
// Development: the real recorder. Production: a no-op that records nothing
|
|
25
|
-
// and whose implementation never enters the bundle.
|
|
26
30
|
const { detach } = attachFlightRecorder({ log: true });
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
than left to the caller, because the failure is silent: a normal import of a
|
|
31
|
-
dev-time tool builds clean, warns about nothing, and ships to every visitor.
|
|
32
|
-
It happened to this project — the recorder's strings were found in a
|
|
33
|
-
production chunk of flemo.dev and had to be removed.
|
|
34
|
-
|
|
35
|
-
Two things to know:
|
|
36
|
-
|
|
37
|
-
- **Not every bundler sets those conditions.** Vite and Next do. If yours does
|
|
38
|
-
not, keep the module behind a dynamic import guarded by a build-time
|
|
39
|
-
constant (`process.env.NODE_ENV !== "production"`, Vite's
|
|
40
|
-
`import.meta.env.DEV`) so the branch — and the module behind it — is
|
|
41
|
-
eliminated. There is no automatic fallback for that case, and the shape of
|
|
42
|
-
this package is why: `@tanstack/react-query-devtools` gets one by importing
|
|
43
|
-
its implementation statically and swapping it on `process.env.NODE_ENV`, so
|
|
44
|
-
the bundler folds the constant and drops the now-unreferenced module. That
|
|
45
|
-
works because their implementation stays a separate module file. Ours cannot
|
|
46
|
-
— `dist/index.mjs` is self-contained on purpose, so it can be loaded
|
|
47
|
-
directly in a page — and measured with esbuild (which sets neither
|
|
48
|
-
condition), the same swap strips nothing: production and development bundles
|
|
49
|
-
came out byte-identical in size with the recorder present in both. The guard
|
|
50
|
-
is the answer there, not a trick inside the package.
|
|
51
|
-
- **`@flemo/devtools/force` is the escape hatch.** It resolves to the real
|
|
52
|
-
tool whatever the build mode, for when you deliberately want the recorder in
|
|
53
|
-
a production build (a staging deploy, an e2e suite that must run against a
|
|
54
|
-
production build). Import it dynamically behind your own opt-in flag.
|
|
55
|
-
|
|
56
|
-
## Keeping it out of your production bundle
|
|
57
|
-
|
|
58
|
-
Install it as a **devDependency** — and know that this alone is not enough.
|
|
59
|
-
`devDependencies` decides what gets INSTALLED (consumers of _your_ package do
|
|
60
|
-
not receive it); it does not decide what gets BUNDLED. A plain top-level import
|
|
61
|
-
of a package you call at runtime ships to every visitor regardless of which
|
|
62
|
-
dependency field it sits in. We measured exactly that on the flemo docs site
|
|
63
|
-
before fixing it.
|
|
64
|
-
|
|
65
|
-
What actually removes it is a **dynamic import behind a build-time constant**,
|
|
66
|
-
which bundlers replace before dead-code elimination:
|
|
33
|
+
Vite and Next set these conditions. For bundlers that do not, guard a dynamic import with a build-time constant:
|
|
67
34
|
|
|
68
35
|
```ts
|
|
69
36
|
// Vite
|
|
@@ -79,272 +46,80 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
79
46
|
}
|
|
80
47
|
```
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
so nothing is pulled in by the import statement itself — but a binding you
|
|
84
|
-
actually call cannot be shaken out. Verify with a production build: the string
|
|
85
|
-
`present-pipeline pacing` (from the blind-spot list) must not appear in it.
|
|
49
|
+
Install as a devDependency, but do not rely on dependency fields for bundle exclusion. A used top-level import can ship even though the package has `"sideEffects": false` and no import-time effects. `dist/index.mjs` is self-contained; an internal `process.env.NODE_ENV` substitution did not reduce measured esbuild bundles when no export condition was set. Use the guarded import and verify production output lacks `present-pipeline pacing`. See `apps/web/app/[lang]/playground/_hooks/useDevtoolsRecorder`.
|
|
86
50
|
|
|
87
|
-
|
|
88
|
-
working reference.
|
|
51
|
+
`@flemo/devtools/force` always loads the recorder. Import it dynamically behind an explicit opt-in only for staging or production-build E2E.
|
|
89
52
|
|
|
90
|
-
##
|
|
53
|
+
## Visual panel
|
|
91
54
|
|
|
92
55
|
```ts
|
|
93
|
-
import {
|
|
56
|
+
import { attachDevtoolsPanel } from "@flemo/devtools";
|
|
57
|
+
const panel = attachDevtoolsPanel();
|
|
58
|
+
// panel.detach();
|
|
59
|
+
```
|
|
94
60
|
|
|
95
|
-
|
|
61
|
+
Consumers must opt in behind a development-only flag; flemo and its playground neither attach nor ship the panel automatically. The floating toggle shows flight count and an anomaly dot. The drawer shows flights, details, active overrides, and blind spots.
|
|
96
62
|
|
|
97
|
-
|
|
63
|
+
Options are `recorder`, `initialOpen` (`false`), and `position` (`"bottom-right"` or `"bottom-left"`). Without `recorder`, the panel reuses this package's `window.flemo` or owns a new recorder. It is idempotent while mounted and inert without a DOM.
|
|
98
64
|
|
|
99
|
-
|
|
100
|
-
recorder.detach();
|
|
101
|
-
```
|
|
65
|
+
The framework-free panel uses an open shadow root. Its fixed, zero-sized host has `data-flemo-devtools-panel`, no screen `data-flemo-*` attributes, and cannot join a flight. Drawer height persists as `flemo:devtools-panel-height` in `sessionStorage`.
|
|
102
66
|
|
|
103
|
-
The
|
|
104
|
-
that name, or you pass `installGlobal: false`):
|
|
67
|
+
The panel must not repaint during a flight:
|
|
105
68
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
69
|
+
- Refresh with a `setTimeout` chain about three times per second while open and once every two seconds while closed; never keep an rAF loop.
|
|
70
|
+
- Skip refreshes and deferred actions while any screen has transitional `data-flemo-status`, then retry next tick.
|
|
71
|
+
- Render only the toggle while closed.
|
|
72
|
+
- Add no CSS transitions, keyframes, or live-dashboard behavior.
|
|
109
73
|
|
|
110
|
-
|
|
111
|
-
toggle is stored in `sessionStorage` under `flemo:devtools`, so it survives
|
|
112
|
-
navigation; `?devtools=off` disarms it).
|
|
74
|
+
## Report schema v2
|
|
113
75
|
|
|
114
|
-
|
|
115
|
-
calls return the same handle. In non-DOM environments (SSR) it returns an
|
|
116
|
-
inert handle.
|
|
76
|
+
Reports contain:
|
|
117
77
|
|
|
118
|
-
|
|
78
|
+
- `generatedAt`, `version: "2"`, and `environment`: user agent and brands, engine, platform, touch count, DPR, screen and viewport sizes, visual viewport scale, idle `rafCadence`, reduced motion, emulation suspicion, and support for long tasks, element animations, and the player-gap mirror.
|
|
79
|
+
- `overrides.active`: all `flemo:*` keys in both storages, unknown keys, keys cleared since attachment, and retired persisted keys marked inert. Since 2026-08-31, flemo reads no `flemo:*` engine key; `overrides.warnings` explains each residue key.
|
|
80
|
+
- `flights[]`: identity, router, navigation kind, timestamps, duration, detected driver, participants, holds, frame and phase statistics, motion, images, player gaps, long tasks, landing checks, and stable anomaly strings.
|
|
81
|
+
- Session `anomalies`, constant `blindSpots`, and constant `judgingProtocol`.
|
|
119
82
|
|
|
120
|
-
|
|
121
|
-
toggle (flight count, plus a red dot when any flight carries an anomaly) and a
|
|
122
|
-
bottom drawer with the flight list, the per-flight detail, the active
|
|
123
|
-
overrides, and the blind-spot list.
|
|
83
|
+
`driver` is classified per flight as `compiled`, `player`, `mixed`, or `unknown`; never infer it from platform policy. `holds.releasedAtMs` is the last release relative to `t0`. Held work is intentionally absorbed, so frame gaps and long tasks are separated into held and released phases; held gaps do not raise anomalies.
|
|
124
84
|
|
|
125
|
-
|
|
126
|
-
import { attachDevtoolsPanel } from "@flemo/devtools";
|
|
85
|
+
`motion` measures pose advancement independently of frame arrival. It records sampled and stalled frames, `longestStallMs` with an anomaly threshold of 48 ms, `pausedAfterRelease`, and `holdReassertedAtMs`, using animation clocks or inline poses without a style flush. Stationary closing frames are tails, not mid-flight stalls.
|
|
127
86
|
|
|
128
|
-
|
|
129
|
-
// panel.detach();
|
|
130
|
-
```
|
|
87
|
+
`images` records loading at start, additions, completions, held images, and `completedUnheld`; count per image so a held loading image cannot cancel an unheld completion. `longTasks` covers visible motion and `holdLongTasks` covers absorbed work. Landing is audited two rAFs after `COMPLETED` for residual inline transforms, off-viewport rest, statuses stuck over 10 seconds, and orphaned holds. Skip orphan auditing when another flight is running.
|
|
131
88
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- ships no CSS transitions and no keyframes at all.
|
|
160
|
-
|
|
161
|
-
Do not turn it into a live-updating dashboard.
|
|
162
|
-
|
|
163
|
-
## Report schema (version "2")
|
|
164
|
-
|
|
165
|
-
```jsonc
|
|
166
|
-
{
|
|
167
|
-
"generatedAt": "2026-08-17T09:00:00.000Z",
|
|
168
|
-
"version": "2",
|
|
169
|
-
"environment": {
|
|
170
|
-
"userAgent": "…",
|
|
171
|
-
"uaBrands": [{ "brand": "Chromium", "version": "126" }],
|
|
172
|
-
"engine": "blink", // blink | webkit | gecko | unknown
|
|
173
|
-
"platform": "MacIntel",
|
|
174
|
-
"maxTouchPoints": 0,
|
|
175
|
-
"devicePixelRatio": 2,
|
|
176
|
-
"screen": { "width": 1728, "height": 1117 },
|
|
177
|
-
"viewport": { "width": 1280, "height": 720 },
|
|
178
|
-
"visualViewportScale": 1,
|
|
179
|
-
"rafCadence": { "medianGapMs": 16.67, "sampleCount": 20 }, // idle sample at attach
|
|
180
|
-
"reducedMotion": false,
|
|
181
|
-
"emulationSuspected": false, // DevTools device-toolbar signature
|
|
182
|
-
"observation": { "longTasks": true, "elementAnimations": true, "playerGapMirror": false }
|
|
183
|
-
},
|
|
184
|
-
"overrides": {
|
|
185
|
-
// Every flemo:* storage key currently set (both storages), including
|
|
186
|
-
// unknown keys and keys present at attach but cleared since (marked).
|
|
187
|
-
"active": { "flemo:apply": "scrub" },
|
|
188
|
-
// Read these FIRST. A non-empty warnings list means the session does not
|
|
189
|
-
// run stock behavior — especially the driver force pin, which pins EVERY
|
|
190
|
-
// transition and once burned a multi-day investigation as residue.
|
|
191
|
-
"warnings": ["flemo:apply=scrub — opt-in diagnostic active (…)"]
|
|
192
|
-
},
|
|
193
|
-
"driverPolicy": {
|
|
194
|
-
"demotion": null, // localStorage flemo:motion-driver ("css" = player demoted)
|
|
195
|
-
"forcePin": null // sessionStorage flemo:motion-driver-force — non-null = PIN ACTIVE
|
|
196
|
-
},
|
|
197
|
-
"flights": [
|
|
198
|
-
{
|
|
199
|
-
"id": "flight-1",
|
|
200
|
-
"routerId": "…", // when the screen stamps data-flemo-router
|
|
201
|
-
"kind": "PUSH", // PUSH | POP | REPLACE
|
|
202
|
-
"t0": { "ms": 1234.5, "iso": "…" }, // performance.now + wall clock
|
|
203
|
-
"t1": { "ms": 1834.5, "iso": "…" },
|
|
204
|
-
"durationMs": 600,
|
|
205
|
-
// player | compiled | mixed | unknown — detected per flight from the
|
|
206
|
-
// DOM signature (inline animation suppression + advancing inline pose
|
|
207
|
-
// = player; a running flemo-* CSSAnimation = compiled). Never assume a
|
|
208
|
-
// platform always routes one tier; routing policies evolve.
|
|
209
|
-
"driver": "compiled",
|
|
210
|
-
"participants": { "screens": 2, "bars": 0, "decorators": 1, "parts": 0 },
|
|
211
|
-
// data-flemo-anim-hold: releasedAtMs = when the LAST hold released,
|
|
212
|
-
// relative to t0. The engine absorbs heavy commits INTO the hold (the
|
|
213
|
-
// screen is posed, not moving), so everything below is segmented on
|
|
214
|
-
// this boundary.
|
|
215
|
-
"holds": { "kind": "park-under", "releasedAtMs": 120 },
|
|
216
|
-
"frameSamples": {
|
|
217
|
-
"count": 36,
|
|
218
|
-
"medianGapMs": 16.7,
|
|
219
|
-
"maxGapMs": 17.2,
|
|
220
|
-
"longGaps": [],
|
|
221
|
-
// held-phase gaps are absorbed by design — no anomaly ever fires on them;
|
|
222
|
-
"held": { "count": 7, "medianGapMs": 18.1, "maxGapMs": 45.0, "over30Count": 1 },
|
|
223
|
-
// released-phase gaps are visible motion — the anomaly rules key on these.
|
|
224
|
-
"released": { "count": 29, "medianGapMs": 16.7, "maxGapMs": 17.2, "over30Count": 0 }
|
|
225
|
-
},
|
|
226
|
-
// Did it MOVE — a different question from "did frames arrive". A
|
|
227
|
-
// compiled flight is read off its own animation clock, a player flight
|
|
228
|
-
// off the inline pose it writes; neither forces a style flush.
|
|
229
|
-
"motion": {
|
|
230
|
-
"sampledFrames": 29,
|
|
231
|
-
"stalledFrames": 0, // released frames where neither clock nor pose moved
|
|
232
|
-
"longestStallMs": 0, // >= 48ms raises an anomaly
|
|
233
|
-
"pausedAfterRelease": false, // playState went "paused" mid-motion
|
|
234
|
-
"holdReassertedAtMs": null // a hold went back ON after releasing
|
|
235
|
-
},
|
|
236
|
-
// Images inside the participants. One still-loading <img> completing
|
|
237
|
-
// mid-flight costs one skipped present (glass-measured 1:1), which is
|
|
238
|
-
// why the engine holds them; an unheld completion is that regression.
|
|
239
|
-
// completedUnheld is the number that matters, counted PER IMAGE: a
|
|
240
|
-
// held-but-still-loading image must not cancel out an unheld completed
|
|
241
|
-
// one. addedDuringFlight covers images a data commit inserts mid-
|
|
242
|
-
// navigation, which is the case core's image hold also watches for.
|
|
243
|
-
"images": {
|
|
244
|
-
"loadingAtStart": 12,
|
|
245
|
-
"addedDuringFlight": 0,
|
|
246
|
-
"completedDuringFlight": 0,
|
|
247
|
-
"heldDuringFlight": 12,
|
|
248
|
-
"completedUnheld": 0
|
|
249
|
-
},
|
|
250
|
-
"playerGaps": { "maxMs": 42.3, "over30Count": 1 }, // only if the player mirror grew
|
|
251
|
-
"longTasks": [{ "startMs": 1200.0, "durationMs": 180.0 }], // intersecting visible motion
|
|
252
|
-
"holdLongTasks": [], // fully absorbed by the hold — engine working as designed
|
|
253
|
-
"landing": {
|
|
254
|
-
// Audited 2 rAF after COMPLETED:
|
|
255
|
-
"residualInlineTransforms": [], // inline transform/opacity leftovers
|
|
256
|
-
"offViewportAtRest": false, // the blank-viewport (PR #259) signature
|
|
257
|
-
"stuckStatuses": [], // transitional statuses >10s old
|
|
258
|
-
// Hold markers still on the page at rest. Skipped (left empty) when
|
|
259
|
-
// another flight is already running: two frames after a landing, a
|
|
260
|
-
// fast back-to-back navigation legitimately owns holds of its own.
|
|
261
|
-
"orphanedHolds": []
|
|
262
|
-
},
|
|
263
|
-
"anomalies": ["long task 180ms overlapped flight start (opening-swallow risk)"]
|
|
264
|
-
}
|
|
265
|
-
],
|
|
266
|
-
"anomalies": [], // session-level: active pins, emulation, stuck flights
|
|
267
|
-
"blindSpots": ["…"], // constant — see below
|
|
268
|
-
"judgingProtocol": ["…"] // constant — the preconditions a verdict needs
|
|
269
|
-
}
|
|
270
|
-
```
|
|
89
|
+
## Detected defects
|
|
90
|
+
|
|
91
|
+
Stable anomaly signatures include:
|
|
92
|
+
|
|
93
|
+
- `hold re-asserted …ms into the flight`: a stale paused hold was rewritten during motion.
|
|
94
|
+
- `motion stalled …ms mid-flight`: motion froze or froze then leapt despite continuing rAF.
|
|
95
|
+
- `playState=paused`: posed motion stopped rather than suffering frame starvation.
|
|
96
|
+
- `image(s) finished loading mid-flight without a hold`: decode rastered on the moving layer.
|
|
97
|
+
- `hold markers left on the page at rest`: hidden content has no owner to reveal it.
|
|
98
|
+
- `screen resting at from-pose while COMPLETED+active`: blank-viewport landing.
|
|
99
|
+
- `long task …ms overlapped the visible-motion start`: swallowed opening.
|
|
100
|
+
- `transitional status stuck >10s`: the navigation queue remains locked.
|
|
101
|
+
- `active force pin flemo:motion-driver-force=…`: diagnostic residue pins a driver.
|
|
102
|
+
|
|
103
|
+
These defects have occurred with clean frame timing.
|
|
104
|
+
|
|
105
|
+
## Judging protocol and blind spots
|
|
106
|
+
|
|
107
|
+
A valid verdict requires DevTools closed, no capture, real input, emulation off, and known display, refresh rate, HiDPI scaling, and Low Power Mode state. The page cannot verify these, so every report includes `judgingProtocol`. Open DevTools caused 2026-08 residual stutter; capture can suppress symptoms; synthetic dispatch bypasses `pointerdown` gesture behavior.
|
|
108
|
+
|
|
109
|
+
In-page tools cannot observe macOS Chrome present-pipeline pacing on 120 Hz ProMotion (Chromium issues 40062488/345275139), display hardware, compositor-internal present skips, or the post-scale DevTools emulation surface. These remain in `blindSpots`. If a correctly judged report is clean but jank is visible, investigate those layers instead of adding in-page instrumentation.
|
|
110
|
+
|
|
111
|
+
## Hand a report to an agent
|
|
112
|
+
|
|
113
|
+
1. Reproduce once with the recorder attached.
|
|
114
|
+
2. Run `copy(JSON.stringify(window.flemo.report(), null, 2))`.
|
|
115
|
+
3. Paste the JSON into the issue or conversation.
|
|
271
116
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Every rule below exists because the matching defect actually shipped, was
|
|
275
|
-
lived with, and cost days. They share one property: **frame timing was clean
|
|
276
|
-
through all of them.** A recorder that only measured gaps would have called
|
|
277
|
-
each of these sessions healthy.
|
|
278
|
-
|
|
279
|
-
| Signature in `anomalies` | The defect it guards against |
|
|
280
|
-
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
281
|
-
| `hold re-asserted …ms into the flight` | An interleaved commit writing the stale paused hold attribute over a running flight — froze motion ~250ms, intermittently |
|
|
282
|
-
| `motion stalled …ms mid-flight` | Any freeze / freeze-then-leap: the pose stopped advancing while rAF kept ticking |
|
|
283
|
-
| `playState=paused` | The flight was posed and then stopped, rather than starved of frames |
|
|
284
|
-
| `image(s) finished loading mid-flight without a hold` | Warm-side image decode rastering on the sliding layer — one skipped present per decode |
|
|
285
|
-
| `hold markers left on the page at rest` | Orphaned image/arrival holds: content hidden with no owner left to reveal it (the permanently-blank-avatar class) |
|
|
286
|
-
| `screen resting at from-pose while COMPLETED+active` | The blank-viewport landing (PR #259) |
|
|
287
|
-
| `long task …ms overlapped the visible-motion start` | The swallowed opening (씹힘) |
|
|
288
|
-
| `transitional status stuck >10s` | A locked navigation queue — every later navigation is silently ignored |
|
|
289
|
-
| `active force pin flemo:motion-driver-force=…` | A/B residue pinning the whole session to one driver (this one burned a multi-day investigation) |
|
|
290
|
-
|
|
291
|
-
## The judging protocol
|
|
292
|
-
|
|
293
|
-
Every report also carries `judgingProtocol`. It is not derived — the page
|
|
294
|
-
cannot check it — so it is stated instead:
|
|
295
|
-
|
|
296
|
-
- **DevTools closed.** The 2026-08 campaign's entire residual "stutter" was
|
|
297
|
-
the open inspector, verified bidirectionally on the reporting machine. A
|
|
298
|
-
clean report from a DevTools-open session proves nothing.
|
|
299
|
-
- **No screen capture running.** A capture client forces the compositor to a
|
|
300
|
-
steady cadence and _suppresses_ the symptom.
|
|
301
|
-
- **Real input.** Synthetic dispatch never fires `pointerdown`, so it bypasses
|
|
302
|
-
the gesture machinery a real navigation goes through.
|
|
303
|
-
- **Known viewing configuration.** Emulation off; which display, refresh rate,
|
|
304
|
-
HiDPI scaling, Low Power Mode.
|
|
305
|
-
|
|
306
|
-
## Blind spots
|
|
307
|
-
|
|
308
|
-
Every report ends with a constant `blindSpots` list: the layers **no in-page
|
|
309
|
-
instrument can see**, each of which once consumed a real investigation:
|
|
310
|
-
|
|
311
|
-
- macOS Chrome present-pipeline pacing (Chromium issues 40062488/345275139) —
|
|
312
|
-
frames judder on 120Hz ProMotion while every in-page metric reads clean;
|
|
313
|
-
proven with a no-script pure-CSS control page.
|
|
314
|
-
- Display-hardware effects (local dimming, backlight modulation).
|
|
315
|
-
- Compositor-internal present skips invisible to rAF.
|
|
316
|
-
- DevTools device emulation composites to a rescaled surface — instruments
|
|
317
|
-
read the pre-scale surface, the eye watches the post-scale one.
|
|
318
|
-
|
|
319
|
-
If the report is clean and the user still sees jank, the cause lives in one of
|
|
320
|
-
these. Do not chase them with in-page tooling.
|
|
321
|
-
|
|
322
|
-
## Handing a report to an agent
|
|
323
|
-
|
|
324
|
-
1. Reproduce the problem once with the recorder attached (playground:
|
|
325
|
-
`/playground?devtools=on`).
|
|
326
|
-
2. `copy(JSON.stringify(window.flemo.report(), null, 2))` in the console.
|
|
327
|
-
3. Paste the JSON into the issue/conversation.
|
|
328
|
-
|
|
329
|
-
Reading order for the agent: `overrides.warnings` (is this session even
|
|
330
|
-
stock?) → `environment.emulationSuspected` + `rafCadence` (is the observation
|
|
331
|
-
trustworthy? what display cadence?) → per-flight `driver` + `anomalies`
|
|
332
|
-
(which tier ran, what went wrong, when) → `blindSpots` (what not to chase).
|
|
333
|
-
All anomaly strings are stable, grep-friendly signatures.
|
|
117
|
+
Read `overrides.warnings`, `environment.emulationSuspected`, `rafCadence`, each flight's `driver` and `anomalies`, then `blindSpots`.
|
|
334
118
|
|
|
335
119
|
## API
|
|
336
120
|
|
|
337
|
-
- `attachFlightRecorder(options?)`
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
-
|
|
341
|
-
|
|
342
|
-
Idempotent while mounted; inert without a DOM.
|
|
343
|
-
- Pure helpers (unit-testable, no DOM): `deriveFlightAnomalies`,
|
|
344
|
-
`deriveReportAnomalies`, `deriveOverrideWarnings`, `classifyDriver`,
|
|
345
|
-
`computeFrameStats`, `computePlayerGapStats`, `parseTranslateX`,
|
|
346
|
-
`kindFromStatus`.
|
|
347
|
-
- Constants/registries: `BLIND_SPOTS`, `FLAG_REGISTRY`, `LONG_GAP_MS`,
|
|
348
|
-
`STUCK_STATUS_MS`, `REPORT_SCHEMA_VERSION`.
|
|
349
|
-
- Environment probes: `captureEnvironment`, `detectEngine`,
|
|
350
|
-
`isEmulationSuspected`, `sampleRafCadence`.
|
|
121
|
+
- `attachFlightRecorder(options?)` returns `{ report(), detach() }`; options: `maxFlights` (50), `log` (`false`), `installGlobal` (`true`).
|
|
122
|
+
- `attachDevtoolsPanel(options?)` returns `{ detach() }`; options: `recorder`, `initialOpen` (`false`), `position` (`"bottom-right"`).
|
|
123
|
+
- Pure helpers: `deriveFlightAnomalies`, `deriveReportAnomalies`, `deriveOverrideWarnings`, `classifyDriver`, `computeFrameStats`, `computePlayerGapStats`, `parseTranslateX`, `kindFromStatus`.
|
|
124
|
+
- Constants and registries: `BLIND_SPOTS`, `FLAG_REGISTRY`, `LONG_GAP_MS`, `STUCK_STATUS_MS`, `REPORT_SCHEMA_VERSION`.
|
|
125
|
+
- Environment probes: `captureEnvironment`, `detectEngine`, `isEmulationSuspected`, `sampleRafCadence`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/anomalies.d.ts
CHANGED
|
@@ -32,10 +32,6 @@ export interface FlightAnomalyInput {
|
|
|
32
32
|
}
|
|
33
33
|
export declare const deriveFlightAnomalies: (input: FlightAnomalyInput) => string[];
|
|
34
34
|
export interface ReportAnomalyInput {
|
|
35
|
-
forcePin: string | null;
|
|
36
|
-
legacyLocalForcePin: string | null;
|
|
37
|
-
/** Force-pin value seen at attach but cleared by report time, if any. */
|
|
38
|
-
clearedForcePin: string | null;
|
|
39
35
|
emulationSuspected: boolean;
|
|
40
36
|
platform: string;
|
|
41
37
|
/** True when a flight is still transitional past STUCK_STATUS_MS. */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const SCREEN_ATTR = "data-flemo-screen";
|
|
2
|
+
export declare const STATUS_ATTR = "data-flemo-status";
|
|
3
|
+
export declare const ACTIVE_ATTR = "data-flemo-active";
|
|
4
|
+
export declare const ROUTER_ATTR = "data-flemo-router";
|
|
5
|
+
export declare const ANIM_HOLD_ATTR = "data-flemo-anim-hold";
|
|
6
|
+
export declare const IMAGE_HOLD_ATTR = "data-flemo-img-hold";
|
|
7
|
+
export declare const HELD_ARRIVAL_ATTR = "data-flemo-held-arrival";
|
|
8
|
+
export declare const PART_NAME_ATTR = "data-flemo-part-name";
|
|
9
|
+
export declare const DECORATOR_ATTR = "data-flemo-decorator";
|
|
10
|
+
export declare const BAR_ATTR = "data-flemo-bar";
|
|
11
|
+
export declare const BAR_STATUS_ATTR = "data-flemo-bar-status";
|
|
12
|
+
export declare const BAR_RIDING_ATTR = "data-flemo-bar-riding";
|
|
13
|
+
/** This package's own marker — core reserves the name but never writes it. */
|
|
14
|
+
export declare const DEVTOOLS_PANEL_ATTR = "data-flemo-devtools-panel";
|
|
15
|
+
/** The statuses during which a flight is moving. */
|
|
16
|
+
export declare const TRANSITIONAL_STATUSES: readonly ["PUSHING", "POPPING", "REPLACING"];
|
|
17
|
+
/** The ANIM_HOLD_ATTR values that mean "held" (any form of park included). */
|
|
18
|
+
export declare const HOLD_VALUES: readonly ["true", "park", "park-under", "park-over"];
|
|
19
|
+
/** `[data-flemo-screen]` */
|
|
20
|
+
export declare const attrSelector: (attribute: string) => string;
|
|
21
|
+
/** `[data-flemo-status="PUSHING"]` */
|
|
22
|
+
export declare const attrValueSelector: (attribute: string, value: string) => string;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export { BLIND_SPOTS } from './blindSpots';
|
|
|
6
6
|
export { attachDevtoolsPanel } from './panel';
|
|
7
7
|
export type { DevtoolsPanelHandle, DevtoolsPanelOptions } from './panel';
|
|
8
8
|
export { captureEnvironment, detectEngine, isEmulationSuspected, sampleRafCadence } from './environment';
|
|
9
|
-
export { deriveOverrideWarnings, FLAG_REGISTRY,
|
|
10
|
-
export type { FlagClass, FlagDescriptor } from './overrides';
|
|
9
|
+
export { CORE_FLAGS, deriveOverrideWarnings, DEVTOOLS_OWNED_FLAGS, FLAG_REGISTRY, PANEL_HEIGHT_KEY, RETIRED_FLAGS, RETIRED_MARKER, snapshotOverrides } from './overrides';
|
|
10
|
+
export type { FlagClass, FlagDescriptor, RetiredFlag } from './overrides';
|
|
11
11
|
export { classifyDriver, computeFrameStats, computePhaseStats, computePlayerGapStats, kindFromStatus, parseTranslateX } from './sampling';
|
|
12
12
|
export type { DriverEvidence } from './sampling';
|
|
13
|
-
export type {
|
|
13
|
+
export type { EnvironmentFingerprint, FlemoReport, FlightDriver, FlightHolds, FlightKind, FlightParticipants, FlightRecord, FlightRecorderHandle, FlightRecorderOptions, FlightTimestamp, FramePhaseStats, FrameSampleStats, LandingAudit, LongTaskSpan, ObservationCapabilities, OverridesSection, PlayerGapStats, UaBrand } from './types';
|