@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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @flemo/devtools
2
2
 
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.
3
+ Zero-dependency flight recorder, on-device readout and visual panel for [flemo](https://flemo.dev) transitions. It observes existing `data-flemo-*` surfaces, leftover `flemo:*` keys, CSS animation events, pointer events, `MutationObserver`, `PerformanceObserver("longtask")`, and rAF. It imports neither `@flemo/core` nor `@flemo/react` and does not alter measured motion.
4
+
5
+ Everything in it is one probe module per question — pacing, motion, images, shared elements, one-frame events, landing residue — behind a small orchestrator. Adding a measurement means adding a probe.
4
6
 
5
7
  ## Quickstart
6
8
 
@@ -10,16 +12,19 @@ import { attachFlightRecorder } from "@flemo/devtools";
10
12
  const recorder = attachFlightRecorder({ log: true });
11
13
  // ...navigate...
12
14
  const report = recorder.report(); // JSON-serializable FlemoReport
15
+ recorder.mark("A"); // label the flights that follow, for a comparison
13
16
  recorder.detach();
14
17
  ```
15
18
 
19
+ Read `report.verdict` first. It is the recorder's own reading of the session in plain sentences, and it refuses to summarise data from a session that was not allowed to produce evidence.
20
+
16
21
  Unless `installGlobal: false` or the name is already owned, the recorder installs `window.flemo`:
17
22
 
18
23
  ```js
19
24
  copy(JSON.stringify(window.flemo.report(), null, 2));
20
25
  ```
21
26
 
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.
27
+ Use `/playground?devtools=on` to enable the playground recorder and its surfaces. `flemo:devtools` persists in `sessionStorage`; `?devtools=off` disables it. Armed, the playground imports `@flemo/devtools/force`, and that is the part worth copying: a plain import of this package resolves to the inert production entry, so an instrument wired the ordinary way exists only in the builds whose numbers the judging protocol below says do not count. `attachFlightRecorder()` is idempotent while attached and returns an inert handle during SSR.
23
28
 
24
29
  ## Production safety
25
30
 
@@ -50,6 +55,24 @@ Install as a devDependency, but do not rely on dependency fields for bundle excl
50
55
 
51
56
  `@flemo/devtools/force` always loads the recorder. Import it dynamically behind an explicit opt-in only for staging or production-build E2E.
52
57
 
58
+ ## On-device readout
59
+
60
+ ```ts
61
+ import { attachDevtoolsHud } from "@flemo/devtools";
62
+ const hud = attachDevtoolsHud({ position: "top" });
63
+ // hud.detach();
64
+ ```
65
+
66
+ A phone has no console. The readout is one monospaced line, high contrast and readable in a photograph of the device:
67
+
68
+ ```
69
+ POP 412ms gap 33.4 drop 1 !2
70
+ ```
71
+
72
+ Tap it for the detail block (frames, motion, holds, shared elements, what drove the navigation, and the flight's anomalies); long-press to cycle the comparison bucket. Options are `recorder`, `position` (`"top"` or `"bottom"`), `initialExpanded` (`false`) and `buckets` (`["A", "B"]`).
73
+
74
+ It obeys the same rules the panel does: it repaints only between flights, its stylesheet carries no transition and no keyframe, and its host is a zero-sized fixed element that cannot join a flight.
75
+
53
76
  ## Visual panel
54
77
 
55
78
  ```ts
@@ -60,7 +83,7 @@ const panel = attachDevtoolsPanel();
60
83
 
61
84
  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.
62
85
 
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.
86
+ Options are `recorder`, `initialOpen` (`false`), `position` (`"bottom-right"` or `"bottom-left"`) and `buckets` (`["A", "B"]`). 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. The header leads with the verdict and every failed precondition, and carries an A/B button that arms the comparison buckets.
64
87
 
65
88
  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`.
66
89
 
@@ -71,19 +94,29 @@ The panel must not repaint during a flight:
71
94
  - Render only the toggle while closed.
72
95
  - Add no CSS transitions, keyframes, or live-dashboard behavior.
73
96
 
74
- ## Report schema v2
97
+ ## Report schema v3
75
98
 
76
99
  Reports contain:
77
100
 
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.
101
+ - `generatedAt`, `version: "3"`, and `verdict`: the session read back in sentences, most important first.
102
+ - `preconditions[]`: the observable half of the judging protocol, each `ok`, `violated` or `unknown` with the reasoning and its numbers. The traps a page cannot see stay `unknown` forever rather than being guessed at.
103
+ - `environment`: user agent and brands, engine, platform, touch count, DPR, hardware concurrency, screen and viewport sizes, visual viewport scale, idle `rafCadence`, reduced motion, development-server globals, emulation suspicion, and what the recorder could observe (long tasks, element animations, and whether its own animation channel ever fired).
79
104
  - `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.
105
+ - `flights[]`: identity, router, bucket, navigation kind, timestamps, duration, detected driver, participants, holds, frame and phase statistics, motion, images, shared elements, tripwire hits, what drove it, long tasks, landing checks, and stable anomaly strings.
106
+ - `comparison[]`: per-bucket medians, worst gaps, drops, anomalies and stalls; empty until `mark()` arms a label.
107
+ - `previousSession`: flights carried across the last full page load, kept apart from the live ones.
81
108
  - Session `anomalies`, constant `blindSpots`, and constant `judgingProtocol`.
82
109
 
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.
110
+ `driver` is classified per flight as `compiled`, `inline`, `mixed`, or `unknown`; never infer it from platform policy. flemo compiles every animation, so `inline` means something else is writing frames onto a participant. `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.
84
111
 
85
112
  `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.
86
113
 
114
+ `morphs` answers the question a shared element cannot answer for itself. A morph that does not pair produces no error, no attribute and no animation, so the runtime writes the pairing key onto every registered morph (`data-flemo-morph-id`) and this section groups the ends: `pairable` had everything they needed, `flew` were stamped with a flight role, and `skipped` is the difference. It also reports duplicate keys inside one screen (a consumer mistake, not a runtime one) and the residue a landing left behind: roles, stand-ins, ghosts, elements stranded in a flight layer, and keyframe rules never dropped.
115
+
116
+ `tripwires` are events the browser REPORTED rather than samples the recorder took: a cancelled flemo animation, an `animationend` carrying `elapsedTime` 0, a hold re-asserted after its release, a ghost cut inside a frame. A sampler cannot see a defect that lasts one frame; a listener cannot miss it.
117
+
118
+ `input` records the trusted and synthetic pointer events around the flight and the pointer types among them. A session driven only by script never fires the gesture machinery, and a session driven only by a mouse never exercises the touch path.
119
+
87
120
  `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.
88
121
 
89
122
  ## Detected defects
@@ -98,13 +131,16 @@ Stable anomaly signatures include:
98
131
  - `screen resting at from-pose while COMPLETED+active`: blank-viewport landing.
99
132
  - `long task …ms overlapped the visible-motion start`: swallowed opening.
100
133
  - `transitional status stuck >10s`: the navigation queue remains locked.
134
+ - `shared element(s) did not fly`: both ends were registered on two screens and neither took a flight role.
135
+ - `morph element(s) still carry a flight role at rest`: a stranded participant that poisons the next pairing.
136
+ - `tripwire zero-length-animation-end`: something landed on an animation that never ran.
101
137
  - `active force pin flemo:motion-driver-force=…`: diagnostic residue pins a driver.
102
138
 
103
139
  These defects have occurred with clean frame timing.
104
140
 
105
141
  ## Judging protocol and blind spots
106
142
 
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.
143
+ A valid verdict requires DevTools closed, no capture, real input, emulation off, a production build, an idle machine, and known display, refresh rate, HiDPI scaling and Low Power Mode state. The ones a page CAN check are checked and appear in `preconditions` — emulation, display cadence, foreground, machine contention, build mode, real and touch input, reduced motion. The rest stay `unknown` there and are stated in `judgingProtocol`. Open DevTools caused 2026-08 residual stutter; capture can suppress symptoms; synthetic dispatch bypasses `pointerdown` gesture behavior.
108
144
 
109
145
  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
146
 
@@ -114,12 +150,14 @@ In-page tools cannot observe macOS Chrome present-pipeline pacing on 120 Hz ProM
114
150
  2. Run `copy(JSON.stringify(window.flemo.report(), null, 2))`.
115
151
  3. Paste the JSON into the issue or conversation.
116
152
 
117
- Read `overrides.warnings`, `environment.emulationSuspected`, `rafCadence`, each flight's `driver` and `anomalies`, then `blindSpots`.
153
+ Read `verdict`, then `preconditions`, then each flight's `anomalies`, then `blindSpots`. A number from a session with a violated precondition is not evidence.
118
154
 
119
155
  ## API
120
156
 
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`.
157
+ - `attachFlightRecorder(options?)` returns `{ report(), mark(), detach() }`; options: `maxFlights` (50), `log` (`false`), `installGlobal` (`true`), `persist` (`true`).
158
+ - `attachDevtoolsHud(options?)` returns `{ detach() }`; options: `recorder`, `position` (`"top"`), `initialExpanded` (`false`), `buckets`.
159
+ - `attachDevtoolsPanel(options?)` returns `{ detach() }`; options: `recorder`, `initialOpen` (`false`), `position` (`"bottom-right"`), `buckets`.
160
+ - Pure helpers: `deriveFlightAnomalies`, `deriveReportAnomalies`, `deriveOverrideWarnings`, `derivePreconditions`, `deriveVerdict`, `summariseBuckets`, `classifyDriver`, `computeFrameStats`, `parseTranslateX`, `kindFromStatus`.
161
+ - Constants and registries: `BLIND_SPOTS`, `JUDGING_PROTOCOL`, `FLAG_REGISTRY`, `LONG_GAP_MS`, `STALL_MS`, `STUCK_STATUS_MS`, `REPORT_SCHEMA_VERSION`.
162
+ - Environment probes: `captureEnvironment`, `detectEngine`, `developmentHints`, `isEmulationSuspected`, `sampleRafCadence`.
163
+ - Trace storage: `loadTrace`, `saveTrace`, `clearTrace`, `TRACE_KEY`.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { FlightDriver, FrameSampleStats, ImageActivity, LandingAudit, LongTaskSpan, MotionProgress, PlayerGapStats } from './types';
1
+ import { FlightDriver, FrameSampleStats, ImageActivity, LandingAudit, LongTaskSpan, MorphActivity, MotionProgress, TripwireHit } from './types';
2
2
  /** A gap at/over this is at least one missed 60Hz frame (mirrors core). */
3
3
  export declare const LONG_GAP_MS = 30;
4
4
  /** A transitional status older than this is a stuck flight. */
@@ -19,7 +19,6 @@ export interface FlightAnomalyInput {
19
19
  t1Ms: number;
20
20
  driver: FlightDriver;
21
21
  frameSamples: FrameSampleStats;
22
- playerGaps: PlayerGapStats | null;
23
22
  /** Long tasks intersecting the RELEASED (visible-motion) phase. */
24
23
  longTasks: LongTaskSpan[];
25
24
  /** Long tasks fully absorbed by the hold phase (informational only). */
@@ -29,6 +28,8 @@ export interface FlightAnomalyInput {
29
28
  landing: LandingAudit;
30
29
  motion: MotionProgress;
31
30
  images: ImageActivity;
31
+ morphs: MorphActivity;
32
+ tripwires: TripwireHit[];
32
33
  }
33
34
  export declare const deriveFlightAnomalies: (input: FlightAnomalyInput) => string[];
34
35
  export interface ReportAnomalyInput {
@@ -0,0 +1,2 @@
1
+ import { BucketSummary, FlightRecord } from './types';
2
+ export declare const summariseBuckets: (flights: readonly FlightRecord[]) => BucketSummary[];
@@ -10,6 +10,28 @@ export declare const DECORATOR_ATTR = "data-flemo-decorator";
10
10
  export declare const BAR_ATTR = "data-flemo-bar";
11
11
  export declare const BAR_STATUS_ATTR = "data-flemo-bar-status";
12
12
  export declare const BAR_RIDING_ATTR = "data-flemo-bar-riding";
13
+ /** A registered morph. The value is the role while it flies: "enter"/"exit". */
14
+ export declare const MORPH_ATTR = "data-flemo-morph";
15
+ /** The pairing key (the binding's `layoutId`), so the two ends can be grouped. */
16
+ export declare const MORPH_ID_ATTR = "data-flemo-morph-id";
17
+ /** The registered morph-transition name, absent/empty meaning the default preset. */
18
+ export declare const MORPH_NAME_ATTR = "data-flemo-morph-name";
19
+ /** The per-Router flight layer a staged morph is lifted into. */
20
+ export declare const MORPH_LAYER_ATTR = "data-flemo-morph-layer";
21
+ /** The copy left in the layout holding the flying element's place. */
22
+ export declare const MORPH_STAND_IN_ATTR = "data-flemo-morph-stand-in";
23
+ /** The copy of the replaced element carried inside the flight. */
24
+ export declare const MORPH_GHOST_ATTR = "data-flemo-morph-ghost";
25
+ /** A screen being driven as a camera by a morph, stamped with the flight id. */
26
+ export declare const MORPH_CAMERA_ATTR = "data-flemo-morph-camera";
27
+ /** The `<style>` element a morph writes its per-flight keyframes into. */
28
+ export declare const MORPH_SHEET_ATTR = "data-flemo-morph-sheet";
29
+ /** The values MORPH_ATTR takes while an element is in the air. */
30
+ export declare const MORPH_ROLES: readonly ["enter", "exit"];
31
+ /** Every flemo keyframe name starts with this. */
32
+ export declare const FLEMO_ANIMATION_PREFIX = "flemo-";
33
+ /** A morph's per-flight keyframes are namespaced again under this. */
34
+ export declare const MORPH_ANIMATION_PREFIX = "flemo-morph-";
13
35
  /** This package's own marker — core reserves the name but never writes it. */
14
36
  export declare const DEVTOOLS_PANEL_ATTR = "data-flemo-devtools-panel";
15
37
  /** The statuses during which a flight is moving. */
@@ -25,7 +25,10 @@ export declare const sampleRafCadence: (frames?: number) => Promise<{
25
25
  medianGapMs: number | null;
26
26
  sampleCount: number;
27
27
  }>;
28
+ export declare const developmentHints: () => string[];
28
29
  export declare const captureEnvironment: (rafCadence: {
29
30
  medianGapMs: number | null;
30
31
  sampleCount: number;
31
- }) => EnvironmentFingerprint;
32
+ },
33
+ /** Whether the tripwires have seen a flemo animation event (see types). */
34
+ animationEvents?: boolean) => EnvironmentFingerprint;
@@ -0,0 +1,40 @@
1
+ import { FrameProbeState } from './frameProbe';
2
+ import { ImageProbeState } from './imageProbe';
3
+ import { MorphProbeState } from './morphProbe';
4
+ import { FlightKind, FlightRecord, TripwireHit } from './types';
5
+ /**
6
+ * The mutable state of one flight while it is in the air.
7
+ *
8
+ * Composed of one field per probe rather than a flat bag: every probe owns its
9
+ * own state shape, the orchestrator owns only the lifecycle fields, and adding
10
+ * a measurement means adding a field and a module beside it rather than
11
+ * growing a closure nobody can hold in their head.
12
+ */
13
+ export interface ActiveFlight {
14
+ id: string;
15
+ kind: FlightKind;
16
+ routerId?: string;
17
+ /** The comparison bucket armed when this flight opened, if any. */
18
+ bucket: string | null;
19
+ t0Ms: number;
20
+ t0Iso: string;
21
+ elements: Element[];
22
+ participants: FlightRecord["participants"];
23
+ holdKind: string | null;
24
+ holdReleasedAtMs: number | null;
25
+ /**
26
+ * When the first flemo keyframe actually STARTED, relative to t0.
27
+ *
28
+ * The status flip and the first moving frame are not the same moment: a
29
+ * commit, a style recalculation and a present sit between them, and on a
30
+ * phone that gap has measured 90-165ms while every other number stayed
31
+ * clean. It is reported rather than judged, because the gap is React's and
32
+ * the browser's, not the transition's.
33
+ */
34
+ firstAnimationAtMs: number | null;
35
+ frames: FrameProbeState;
36
+ images: ImageProbeState;
37
+ morphs: MorphProbeState;
38
+ tripwires: TripwireHit[];
39
+ rafId: number | null;
40
+ }
@@ -0,0 +1,53 @@
1
+ import { MotionProgress } from './types';
2
+ /** Driver evidence gathered by the rAF sampler during a flight. */
3
+ export interface DriverEvidence {
4
+ /** A running CSSAnimation named flemo-* was observed on a participant. */
5
+ compiledAnimation: boolean;
6
+ /** A participant carried inline `animation` suppression. */
7
+ inlineSuppression: boolean;
8
+ /** Inline transform/opacity advanced between sampled frames. */
9
+ inlineAdvance: boolean;
10
+ }
11
+ /** Everything the frame probe accumulates for one flight. */
12
+ export interface FrameProbeState {
13
+ /** Frame gaps while any participant still carried an active anim-hold. */
14
+ heldGaps: number[];
15
+ /** Frame gaps after every hold released (the visible-motion phase). */
16
+ releasedGaps: number[];
17
+ lastFrameAt: number | null;
18
+ evidence: DriverEvidence;
19
+ lastPose: Map<Element, string>;
20
+ /** Cached compiled animations, so the clock read never re-queries per frame. */
21
+ clocks: Map<Element, Animation>;
22
+ lastClock: Map<Element, number>;
23
+ releasedFrames: number;
24
+ stalledFrames: number;
25
+ tailFrames: number;
26
+ stallRunMs: number;
27
+ longestStallMs: number;
28
+ pausedAfterRelease: boolean;
29
+ holdReassertedAtMs: number | null;
30
+ }
31
+ export declare const createFrameProbeState: () => FrameProbeState;
32
+ /**
33
+ * A hold is active while ANY participating element still carries an active
34
+ * `data-flemo-anim-hold` value; the flight is "released" once every one of
35
+ * them reads "false" (or drops the attribute). The engine deliberately absorbs
36
+ * heavy commits INTO the hold — the screen is posed, not moving — so gaps and
37
+ * long tasks are segmented on this boundary.
38
+ */
39
+ export declare const holdActive: (elements: readonly Element[]) => boolean;
40
+ /**
41
+ * Did this frame MOVE? Read from the cheapest honest source per tier: a
42
+ * compiled flight's own animation clock (getAnimations, no style flush) and an
43
+ * inline-driven flight's pose (already in the style attribute). A frame where
44
+ * neither moved is a stall — the signature that timing metrics miss.
45
+ */
46
+ export declare const sampleProgress: (state: FrameProbeState, elements: readonly Element[], frameGapMs: number) => void;
47
+ export declare const sampleDriverEvidence: (state: FrameProbeState, elements: readonly Element[]) => void;
48
+ /**
49
+ * Everything the frame probe knows about the motion. `firstAnimationAtMs` is
50
+ * deliberately NOT here: it comes from the browser's own `animationstart`
51
+ * event, which is a tripwire, and the orchestrator joins the two.
52
+ */
53
+ export declare const motionProgress: (state: FrameProbeState) => Omit<MotionProgress, "firstAnimationAtMs">;
package/dist/hud.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { FlightRecorderHandle } from './types';
2
+ export interface DevtoolsHudOptions {
3
+ /** Recorder to read. Defaults to this package's `window.flemo`, else its own. */
4
+ recorder?: FlightRecorderHandle;
5
+ /** Where the strip sits. Default "top". */
6
+ position?: "top" | "bottom";
7
+ /** Start expanded. Default false (the one-line summary). */
8
+ initialExpanded?: boolean;
9
+ /** Labels the long-press cycles through. Default ["A", "B"]. */
10
+ buckets?: string[];
11
+ }
12
+ export interface DevtoolsHudHandle {
13
+ detach: () => void;
14
+ }
15
+ /**
16
+ * Mount the on-device readout. Idempotent while mounted; inert without a DOM.
17
+ */
18
+ export declare const attachDevtoolsHud: (options?: DevtoolsHudOptions) => DevtoolsHudHandle;
@@ -0,0 +1,23 @@
1
+ import { ImageActivity } from './types';
2
+ export interface ImageProbeState {
3
+ /** Images being tracked for this flight: loading at t0, plus arrivals. */
4
+ tracked: Set<HTMLImageElement>;
5
+ loadingAtStart: number;
6
+ addedDuringFlight: number;
7
+ held: Set<Element>;
8
+ }
9
+ export declare const createImageProbeState: (screens: readonly Element[]) => ImageProbeState;
10
+ /**
11
+ * Images that arrive DURING the flight — a data commit landing mid-navigation,
12
+ * which is the case core's image hold watches for with its own observer.
13
+ * Without this the probe would only ever see the screens as they looked at t0.
14
+ */
15
+ export declare const trackAddedImages: (state: ImageProbeState, elements: readonly Element[], added: NodeList) => void;
16
+ /** One query per flight, on the first moving frame: which images the engine parked. */
17
+ export declare const snapshotHeldImages: (state: ImageProbeState, elements: readonly Element[]) => void;
18
+ /**
19
+ * Per-image accounting. Counting completions and holds separately and
20
+ * subtracting would cancel a held-but-still-loading image against an unheld
21
+ * completed one, hiding the exact regression this exists to catch.
22
+ */
23
+ export declare const imageActivity: (state: ImageProbeState) => ImageActivity;
package/dist/index.d.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  export { attachFlightRecorder, REPORT_SCHEMA_VERSION } from './recorder';
2
2
  export type { FlemoGlobal } from './recorder';
3
- export { deriveFlightAnomalies, deriveReportAnomalies, LONG_GAP_MS, STUCK_STATUS_MS, OPENING_WINDOW_LEAD_MS, OPENING_WINDOW_TAIL_MS, MID_FLIGHT_TASK_MS } from './anomalies';
3
+ export { deriveFlightAnomalies, deriveReportAnomalies, LONG_GAP_MS, STALL_MS, STUCK_STATUS_MS, OPENING_WINDOW_LEAD_MS, OPENING_WINDOW_TAIL_MS, MID_FLIGHT_TASK_MS } from './anomalies';
4
4
  export type { FlightAnomalyInput, ReportAnomalyInput } from './anomalies';
5
5
  export { BLIND_SPOTS } from './blindSpots';
6
+ export { JUDGING_PROTOCOL } from './judging';
7
+ export { summariseBuckets } from './buckets';
6
8
  export { attachDevtoolsPanel } from './panel';
7
9
  export type { DevtoolsPanelHandle, DevtoolsPanelOptions } from './panel';
8
- export { captureEnvironment, detectEngine, isEmulationSuspected, sampleRafCadence } from './environment';
10
+ export { attachDevtoolsHud } from './hud';
11
+ export type { DevtoolsHudHandle, DevtoolsHudOptions } from './hud';
12
+ export { captureEnvironment, detectEngine, developmentHints, isEmulationSuspected, sampleRafCadence } from './environment';
13
+ export { derivePreconditions } from './preconditions';
14
+ export type { PreconditionInput } from './preconditions';
15
+ export { deriveVerdict } from './verdict';
16
+ export type { VerdictInput } from './verdict';
17
+ export { clearTrace, loadTrace, saveTrace, TRACE_KEY } from './persistence';
9
18
  export { CORE_FLAGS, deriveOverrideWarnings, DEVTOOLS_OWNED_FLAGS, FLAG_REGISTRY, PANEL_HEIGHT_KEY, RETIRED_FLAGS, RETIRED_MARKER, snapshotOverrides } from './overrides';
10
19
  export type { FlagClass, FlagDescriptor, RetiredFlag } from './overrides';
11
- export { classifyDriver, computeFrameStats, computePhaseStats, computePlayerGapStats, kindFromStatus, parseTranslateX } from './sampling';
12
- export type { DriverEvidence } from './sampling';
13
- export type { EnvironmentFingerprint, FlemoReport, FlightDriver, FlightHolds, FlightKind, FlightParticipants, FlightRecord, FlightRecorderHandle, FlightRecorderOptions, FlightTimestamp, FramePhaseStats, FrameSampleStats, LandingAudit, LongTaskSpan, ObservationCapabilities, OverridesSection, PlayerGapStats, UaBrand } from './types';
20
+ export { classifyDriver, computeFrameStats, computePhaseStats, kindFromStatus, parseTranslateX } from './sampling';
21
+ export type { DriverEvidence } from './frameProbe';
22
+ export type { BucketSummary, EnvironmentFingerprint, FlemoReport, FlightDriver, FlightHolds, FlightKind, FlightParticipants, FlightRecord, FlightRecorderHandle, FlightRecorderOptions, FlightTimestamp, FramePhaseStats, FrameSampleStats, ImageActivity, InputEvidence, LandingAudit, LongTaskSpan, MorphActivity, MotionProgress, ObservationCapabilities, OverridesSection, Precondition, PreconditionStatus, PreviousSession, TripwireHit, UaBrand } from './types';