@flemo/devtools 0.1.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/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/__tests__/anomalies.test.d.ts +1 -0
- package/dist/__tests__/environment.test.d.ts +1 -0
- package/dist/__tests__/overrides.test.d.ts +1 -0
- package/dist/__tests__/panel.test.d.ts +1 -0
- package/dist/__tests__/recorder.branches.test.d.ts +1 -0
- package/dist/__tests__/recorder.guards.test.d.ts +1 -0
- package/dist/__tests__/recorder.regressions.test.d.ts +1 -0
- package/dist/__tests__/recorder.test.d.ts +1 -0
- package/dist/__tests__/sampling.test.d.ts +1 -0
- package/dist/anomalies.d.ts +45 -0
- package/dist/blindSpots.d.ts +1 -0
- package/dist/environment.d.ts +31 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +1056 -0
- package/dist/judging.d.ts +1 -0
- package/dist/overrides.d.ts +22 -0
- package/dist/panel/dom.d.ts +7 -0
- package/dist/panel/format.d.ts +29 -0
- package/dist/panel/index.d.ts +25 -0
- package/dist/panel/styles.d.ts +1 -0
- package/dist/panel/view.d.ts +12 -0
- package/dist/recorder.d.ts +17 -0
- package/dist/sampling.d.ts +26 -0
- package/dist/types.d.ts +280 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const JUDGING_PROTOCOL: readonly string[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type FlagClass = "production-state" | "production-default-with-override" | "opt-in-diagnostic";
|
|
2
|
+
export interface FlagDescriptor {
|
|
3
|
+
key: string;
|
|
4
|
+
storage: "session" | "local";
|
|
5
|
+
kind: FlagClass;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const FLAG_REGISTRY: readonly FlagDescriptor[];
|
|
9
|
+
/** Marker suffix for the legacy-location residue of the force pin. */
|
|
10
|
+
export declare const LEGACY_LOCAL_PIN_KEY = "flemo:motion-driver-force (localStorage \u2014 legacy location)";
|
|
11
|
+
/**
|
|
12
|
+
* Snapshot every `flemo:*` key currently set, from both storages: the
|
|
13
|
+
* registry keys from their native storage, the legacy localStorage location
|
|
14
|
+
* of the force pin, and any UNKNOWN `flemo:`-prefixed key either storage
|
|
15
|
+
* holds (a future flag, or hand-set residue).
|
|
16
|
+
*/
|
|
17
|
+
export declare const snapshotOverrides: () => Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Derive prominent warnings from an override snapshot. Pure — testable with
|
|
20
|
+
* a plain record and reusable on merged attach+report snapshots.
|
|
21
|
+
*/
|
|
22
|
+
export declare const deriveOverrideWarnings: (active: Record<string, string>) => string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const el: <K extends keyof HTMLElementTagNameMap>(tag: K, className?: string, text?: string) => HTMLElementTagNameMap[K];
|
|
2
|
+
export declare const svgEl: (tag: string, attributes: Record<string, string>) => SVGElement;
|
|
3
|
+
/** Empty a container without innerHTML (which would re-parse markup). */
|
|
4
|
+
export declare const clear: (node: Element) => void;
|
|
5
|
+
/** Set textContent only when it actually changed — the panel's whole job is
|
|
6
|
+
* to not churn the DOM more than necessary. */
|
|
7
|
+
export declare const setText: (node: Element, value: string) => void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FlemoReport, FlightRecord, FrameSampleStats } from '../types';
|
|
2
|
+
/** What a missing/unreadable value renders as. Never blank — an empty cell
|
|
3
|
+
* reads as "zero", a dash reads as "the recorder did not see this". */
|
|
4
|
+
export declare const DASH = "\u2014";
|
|
5
|
+
export declare const formatText: (value: string | number | null | undefined) => string;
|
|
6
|
+
/** Whole milliseconds — flight durations, long-task spans. */
|
|
7
|
+
export declare const formatMs: (value: number | null | undefined) => string;
|
|
8
|
+
/** One decimal — frame gaps and rAF cadence, where 16.7 vs 17 matters. */
|
|
9
|
+
export declare const formatGapMs: (value: number | null | undefined) => string;
|
|
10
|
+
export declare const formatCount: (value: number | null | undefined) => string;
|
|
11
|
+
export declare const formatBool: (value: boolean | null | undefined) => string;
|
|
12
|
+
/** Header line: the four facts that decide whether the rest is trustworthy —
|
|
13
|
+
* which engine, at what density, in what viewport, at what display cadence. */
|
|
14
|
+
export declare const environmentSummary: (report: FlemoReport | null) => string;
|
|
15
|
+
/** Cheap change key for the flight LIST. Rebuilding rows costs nothing when
|
|
16
|
+
* it is actually needed and everything when it is not, so the list is only
|
|
17
|
+
* rebuilt when one of these visible values moved. */
|
|
18
|
+
export declare const flightListSignature: (flights: readonly FlightRecord[], selectedId: string | null) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Per-frame gap series for the released phase, when the record carries one.
|
|
21
|
+
*
|
|
22
|
+
* The shipped FramePhaseStats is aggregate-only (count / median / max /
|
|
23
|
+
* over30Count), so there is usually nothing to draw and the sparkline is
|
|
24
|
+
* omitted silently. If a future recorder field supplies a numeric series this
|
|
25
|
+
* picks it up without a panel change — but the panel never INVENTS a series
|
|
26
|
+
* it cannot see (a fabricated trace is exactly the kind of artifact that
|
|
27
|
+
* sends an investigation down a blind alley).
|
|
28
|
+
*/
|
|
29
|
+
export declare const releasedGapSeries: (frameSamples: FrameSampleStats | undefined) => number[] | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FlightRecorderHandle } from '../types';
|
|
2
|
+
export interface DevtoolsPanelOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Recorder to read. Defaults to this package's `window.flemo` when one is
|
|
5
|
+
* installed, otherwise the panel attaches its own and detaches it with
|
|
6
|
+
* itself.
|
|
7
|
+
*/
|
|
8
|
+
recorder?: FlightRecorderHandle;
|
|
9
|
+
/** Start with the panel expanded. Default false (toggle button only). */
|
|
10
|
+
initialOpen?: boolean;
|
|
11
|
+
/** Corner for the toggle button. Default "bottom-right". */
|
|
12
|
+
position?: "bottom-right" | "bottom-left";
|
|
13
|
+
}
|
|
14
|
+
export interface DevtoolsPanelHandle {
|
|
15
|
+
detach: () => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Mount the devtools panel: a floating toggle plus a bottom drawer with the
|
|
19
|
+
* flight list and per-flight detail, rendered into a shadow root so no
|
|
20
|
+
* consumer CSS reaches in and none of ours reaches out.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent — while a panel is mounted, further calls return the same
|
|
23
|
+
* handle. In a non-DOM environment it returns an inert handle.
|
|
24
|
+
*/
|
|
25
|
+
export declare const attachDevtoolsPanel: (options?: DevtoolsPanelOptions) => DevtoolsPanelHandle;
|
|
@@ -0,0 +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";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FlemoReport, FlightRecord } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Header chips: override warnings and session-level anomalies first (they
|
|
4
|
+
* decide whether the rest of the report describes stock behavior at all),
|
|
5
|
+
* then the raw active overrides. Values come from storage the user can edit,
|
|
6
|
+
* so they land via textContent like everything else.
|
|
7
|
+
*/
|
|
8
|
+
export declare const renderChips: (node: Element, report: FlemoReport | null) => void;
|
|
9
|
+
/** Newest first — the flight you just ran is the one you came to look at. */
|
|
10
|
+
export declare const renderFlightList: (node: Element, flights: readonly FlightRecord[], selectedId: string | null, onSelect: (id: string) => void) => void;
|
|
11
|
+
export declare const renderFlightDetail: (node: Element, flight: FlightRecord | null) => void;
|
|
12
|
+
export declare const renderBlindSpots: (node: Element, blindSpots: readonly string[] | undefined) => void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FlemoReport, FlightRecord, FlightRecorderHandle, FlightRecorderOptions } from './types';
|
|
2
|
+
export declare const REPORT_SCHEMA_VERSION = "2";
|
|
3
|
+
/** The API installed at window.flemo (guarded — see attachFlightRecorder). */
|
|
4
|
+
export interface FlemoGlobal {
|
|
5
|
+
/** Marker distinguishing this recorder's global from foreign occupants. */
|
|
6
|
+
__flemoDevtools: true;
|
|
7
|
+
report: () => FlemoReport;
|
|
8
|
+
flights: () => FlightRecord[];
|
|
9
|
+
detach: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Attach the flight recorder. Idempotent: while a recorder is attached,
|
|
13
|
+
* further calls return the SAME handle (their options are ignored). In a
|
|
14
|
+
* non-DOM environment it returns an inert handle whose report carries only
|
|
15
|
+
* the schema constants.
|
|
16
|
+
*/
|
|
17
|
+
export declare const attachFlightRecorder: (options?: FlightRecorderOptions) => FlightRecorderHandle;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FlightDriver, FlightKind, FramePhaseStats, FrameSampleStats, PlayerGapStats } from './types';
|
|
2
|
+
export declare const computePhaseStats: (gaps: readonly number[]) => FramePhaseStats;
|
|
3
|
+
/**
|
|
4
|
+
* Combined frame stats: `heldGaps` are frames sampled while any transitional
|
|
5
|
+
* screen still carried an active anim-hold; `releasedGaps` are frames after
|
|
6
|
+
* every hold released. The hold phase precedes release, so the overall
|
|
7
|
+
* ordered gap list is their concatenation.
|
|
8
|
+
*/
|
|
9
|
+
export declare const computeFrameStats: (heldGaps: readonly number[], releasedGaps?: readonly number[]) => FrameSampleStats;
|
|
10
|
+
export declare const computePlayerGapStats: (gaps: readonly number[]) => PlayerGapStats | null;
|
|
11
|
+
/** Driver evidence gathered by the rAF sampler during a flight. */
|
|
12
|
+
export interface DriverEvidence {
|
|
13
|
+
/** A running CSSAnimation named flemo-* was observed on a participant. */
|
|
14
|
+
compiledAnimation: boolean;
|
|
15
|
+
/** A participant carried inline `animation` suppression (player stake). */
|
|
16
|
+
playerSuppression: boolean;
|
|
17
|
+
/** Inline transform/opacity advanced between sampled frames. */
|
|
18
|
+
playerAdvance: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const classifyDriver: (evidence: DriverEvidence) => FlightDriver;
|
|
21
|
+
export declare const kindFromStatus: (status: string) => FlightKind | null;
|
|
22
|
+
/**
|
|
23
|
+
* Horizontal translation of a computed `transform` matrix, in px. Returns
|
|
24
|
+
* null for "none"/"" or unparseable values.
|
|
25
|
+
*/
|
|
26
|
+
export declare const parseTranslateX: (transform: string) => number | null;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/** How a flight's motion was driven, judged from DOM signatures alone. */
|
|
2
|
+
export type FlightDriver = "player" | "compiled" | "mixed" | "unknown";
|
|
3
|
+
/** Navigation kind, from the transitional `data-flemo-status` value. */
|
|
4
|
+
export type FlightKind = "PUSH" | "POP" | "REPLACE";
|
|
5
|
+
/** A single moment on both clocks: monotonic performance.now + wall clock. */
|
|
6
|
+
export interface FlightTimestamp {
|
|
7
|
+
/** performance.now() milliseconds (same clock as longTasks/frame gaps). */
|
|
8
|
+
ms: number;
|
|
9
|
+
/** ISO-8601 wall-clock time, for correlating with external logs. */
|
|
10
|
+
iso: string;
|
|
11
|
+
}
|
|
12
|
+
/** Elements that carried transitional flemo attributes during the flight. */
|
|
13
|
+
export interface FlightParticipants {
|
|
14
|
+
screens: number;
|
|
15
|
+
bars: number;
|
|
16
|
+
decorators: number;
|
|
17
|
+
parts: number;
|
|
18
|
+
}
|
|
19
|
+
/** The animation-hold observed on the flight (data-flemo-anim-hold). */
|
|
20
|
+
export interface FlightHolds {
|
|
21
|
+
/** "park-under" | "park-over" | "park" | "true", or null if no hold ran. */
|
|
22
|
+
kind: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Milliseconds after t0 at which the LAST hold released (every
|
|
25
|
+
* transitional screen's data-flemo-anim-hold at "false"). Null when no
|
|
26
|
+
* hold ran — or when a hold never released (then the whole flight is the
|
|
27
|
+
* held phase).
|
|
28
|
+
*/
|
|
29
|
+
releasedAtMs: number | null;
|
|
30
|
+
}
|
|
31
|
+
/** Frame-gap stats for one phase of a flight (held vs released). */
|
|
32
|
+
export interface FramePhaseStats {
|
|
33
|
+
count: number;
|
|
34
|
+
medianGapMs: number;
|
|
35
|
+
maxGapMs: number;
|
|
36
|
+
over30Count: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Stats over the recorder's own rAF-observed frame gaps during the flight,
|
|
40
|
+
* segmented by the anim-hold phase. The engine deliberately absorbs heavy
|
|
41
|
+
* commits INTO the hold (the screen is posed, not moving), so a gap during
|
|
42
|
+
* `held` is the engine working as designed — only `released` gaps are
|
|
43
|
+
* user-visible jank, and only they drive anomaly rules.
|
|
44
|
+
*/
|
|
45
|
+
export interface FrameSampleStats {
|
|
46
|
+
count: number;
|
|
47
|
+
medianGapMs: number;
|
|
48
|
+
maxGapMs: number;
|
|
49
|
+
/** Every observed gap >= 30ms (a missed 60Hz frame), in order. */
|
|
50
|
+
longGaps: number[];
|
|
51
|
+
/** Frames while any transitional screen still carried an active hold. */
|
|
52
|
+
held: FramePhaseStats;
|
|
53
|
+
/** Frames after every hold released — the phase the eye watches. */
|
|
54
|
+
released: FramePhaseStats;
|
|
55
|
+
}
|
|
56
|
+
/** Stats over the transition player's own gap mirror (__flemoPlayerGaps). */
|
|
57
|
+
export interface PlayerGapStats {
|
|
58
|
+
maxMs: number;
|
|
59
|
+
over30Count: number;
|
|
60
|
+
}
|
|
61
|
+
/** A PerformanceObserver("longtask") entry overlapping the flight window. */
|
|
62
|
+
export interface LongTaskSpan {
|
|
63
|
+
startMs: number;
|
|
64
|
+
durationMs: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the motion actually MOVED, as opposed to whether frames arrived.
|
|
68
|
+
*
|
|
69
|
+
* Frame timing and pose progress are different questions, and the 2026-08
|
|
70
|
+
* campaign turned on the difference: a hold attribute re-asserted over a
|
|
71
|
+
* running flight paused the animation for ~250ms while rAF kept ticking at a
|
|
72
|
+
* perfect 16.7ms — every timing metric clean, the screen frozen. The decisive
|
|
73
|
+
* instrument was a pose encoder, so the recorder carries one: for a compiled
|
|
74
|
+
* flight it reads the animation's own clock, for a player flight the inline
|
|
75
|
+
* pose it writes. Neither forces a style flush.
|
|
76
|
+
*/
|
|
77
|
+
export interface MotionProgress {
|
|
78
|
+
/** Frames sampled during the RELEASED (visible-motion) phase. */
|
|
79
|
+
sampledFrames: number;
|
|
80
|
+
/** Released frames where neither the clock nor the pose moved. */
|
|
81
|
+
stalledFrames: number;
|
|
82
|
+
/** Longest unbroken run of stalled released frames, in ms. */
|
|
83
|
+
longestStallMs: number;
|
|
84
|
+
/** A compiled animation reported playState "paused" after its release. */
|
|
85
|
+
pausedAfterRelease: boolean;
|
|
86
|
+
/** Offset from t0 at which a hold was re-asserted AFTER the release. */
|
|
87
|
+
holdReassertedAtMs: number | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Images inside the flight's participants. A still-loading <img> that
|
|
91
|
+
* finishes DURING the flight decodes and first-rasters on the moving layer —
|
|
92
|
+
* glass-measured at one skipped present per decode (2026-08-18). The engine
|
|
93
|
+
* holds those images for the flight span; an unheld one completing mid-flight
|
|
94
|
+
* is that regression coming back.
|
|
95
|
+
*/
|
|
96
|
+
export interface ImageActivity {
|
|
97
|
+
/** Participant images not yet complete when the flight opened. */
|
|
98
|
+
loadingAtStart: number;
|
|
99
|
+
/**
|
|
100
|
+
* Images that appeared INSIDE a participant after the flight opened and
|
|
101
|
+
* were still loading — a data commit landing mid-navigation. The engine's
|
|
102
|
+
* own image hold watches for exactly these, so the recorder must too.
|
|
103
|
+
*/
|
|
104
|
+
addedDuringFlight: number;
|
|
105
|
+
/** Of the tracked images, how many completed before the flight ended. */
|
|
106
|
+
completedDuringFlight: number;
|
|
107
|
+
/** Tracked images seen carrying the engine's hold marker. */
|
|
108
|
+
heldDuringFlight: number;
|
|
109
|
+
/**
|
|
110
|
+
* The number that actually matters: images that completed during the
|
|
111
|
+
* flight WITHOUT a hold, counted per image. Subtracting the two counts
|
|
112
|
+
* above would cancel out a held-but-still-loading image against an
|
|
113
|
+
* unheld completed one and report nothing.
|
|
114
|
+
*/
|
|
115
|
+
completedUnheld: number;
|
|
116
|
+
}
|
|
117
|
+
/** Post-landing residue audit, taken 2 rAF after the flight completed. */
|
|
118
|
+
export interface LandingAudit {
|
|
119
|
+
/**
|
|
120
|
+
* Inline transform/opacity left on participating [data-flemo-screen]
|
|
121
|
+
* elements at rest. The landed scope belongs to the compiled rest rules —
|
|
122
|
+
* any inline pose here is a cleanup failure (the flemo PR #259 class).
|
|
123
|
+
*/
|
|
124
|
+
residualInlineTransforms: string[];
|
|
125
|
+
/**
|
|
126
|
+
* A COMPLETED+active screen whose computed transform translates it >= 50%
|
|
127
|
+
* of the viewport width off screen: the blank-viewport signature.
|
|
128
|
+
*/
|
|
129
|
+
offViewportAtRest: boolean;
|
|
130
|
+
/** Transitional statuses still present ~10s after the flight began. */
|
|
131
|
+
stuckStatuses: string[];
|
|
132
|
+
/**
|
|
133
|
+
* Engine hold markers still on the page at rest. Every hold is supposed to
|
|
134
|
+
* be released when the flight lands; a leftover marker means something is
|
|
135
|
+
* still hidden with no owner left to reveal it — the class that produced
|
|
136
|
+
* ~130 permanently blank avatars before the single-owner guard landed.
|
|
137
|
+
*/
|
|
138
|
+
orphanedHolds: string[];
|
|
139
|
+
}
|
|
140
|
+
/** One recorded navigation flight. */
|
|
141
|
+
export interface FlightRecord {
|
|
142
|
+
/** Sequential id, "flight-1"… in recording order. */
|
|
143
|
+
id: string;
|
|
144
|
+
/** data-flemo-router of the first participating screen, if stamped. */
|
|
145
|
+
routerId?: string;
|
|
146
|
+
kind: FlightKind;
|
|
147
|
+
t0: FlightTimestamp;
|
|
148
|
+
t1: FlightTimestamp;
|
|
149
|
+
durationMs: number;
|
|
150
|
+
driver: FlightDriver;
|
|
151
|
+
participants: FlightParticipants;
|
|
152
|
+
holds: FlightHolds;
|
|
153
|
+
frameSamples: FrameSampleStats;
|
|
154
|
+
/** Did the motion advance, frame by frame — not just: did frames arrive. */
|
|
155
|
+
motion: MotionProgress;
|
|
156
|
+
/** Image load/hold activity inside the participants during the flight. */
|
|
157
|
+
images: ImageActivity;
|
|
158
|
+
/** Present only when the player's gap mirror grew during the flight. */
|
|
159
|
+
playerGaps?: PlayerGapStats;
|
|
160
|
+
/**
|
|
161
|
+
* Long tasks intersecting the RELEASED phase (visible motion) — these
|
|
162
|
+
* drive the anomaly rules.
|
|
163
|
+
*/
|
|
164
|
+
longTasks: LongTaskSpan[];
|
|
165
|
+
/**
|
|
166
|
+
* Long tasks fully absorbed by the hold phase: the screen was posed, not
|
|
167
|
+
* moving, so these are the engine's commit-absorption working as designed,
|
|
168
|
+
* not user-visible jank.
|
|
169
|
+
*/
|
|
170
|
+
holdLongTasks: LongTaskSpan[];
|
|
171
|
+
landing: LandingAudit;
|
|
172
|
+
/** Human/agent-readable findings derived from the data above. */
|
|
173
|
+
anomalies: string[];
|
|
174
|
+
}
|
|
175
|
+
export interface UaBrand {
|
|
176
|
+
brand: string;
|
|
177
|
+
version: string;
|
|
178
|
+
}
|
|
179
|
+
/** What this recorder could actually observe in this browser. */
|
|
180
|
+
export interface ObservationCapabilities {
|
|
181
|
+
/** PerformanceObserver("longtask") supported — longTasks are meaningful. */
|
|
182
|
+
longTasks: boolean;
|
|
183
|
+
/** Element.getAnimations available — compiled-tier detection is direct. */
|
|
184
|
+
elementAnimations: boolean;
|
|
185
|
+
/** window.__flemoPlayerGaps present (the player has driven >= 1 flight). */
|
|
186
|
+
playerGapMirror: boolean;
|
|
187
|
+
}
|
|
188
|
+
export interface EnvironmentFingerprint {
|
|
189
|
+
userAgent: string;
|
|
190
|
+
/** navigator.userAgentData.brands, when the browser ships UA-CH. */
|
|
191
|
+
uaBrands: UaBrand[] | null;
|
|
192
|
+
engine: "blink" | "webkit" | "gecko" | "unknown";
|
|
193
|
+
platform: string;
|
|
194
|
+
maxTouchPoints: number;
|
|
195
|
+
devicePixelRatio: number;
|
|
196
|
+
screen: {
|
|
197
|
+
width: number;
|
|
198
|
+
height: number;
|
|
199
|
+
};
|
|
200
|
+
viewport: {
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
};
|
|
204
|
+
visualViewportScale: number | null;
|
|
205
|
+
/** Idle rAF cadence sampled at attach (median gap over ~20 frames). */
|
|
206
|
+
rafCadence: {
|
|
207
|
+
medianGapMs: number | null;
|
|
208
|
+
sampleCount: number;
|
|
209
|
+
};
|
|
210
|
+
reducedMotion: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* DevTools device-emulation signature: Blink + desktop platform + touch
|
|
213
|
+
* points. Emulation composites the page to a scaled surface, so VISUAL
|
|
214
|
+
* reports from such a session are untrustworthy (instruments read the
|
|
215
|
+
* pre-scale surface and say "clean" while the eye watches the post-scale
|
|
216
|
+
* one). Mirrors packages/core/src/core/engine/emulationNotice.ts.
|
|
217
|
+
*/
|
|
218
|
+
emulationSuspected: boolean;
|
|
219
|
+
observation: ObservationCapabilities;
|
|
220
|
+
}
|
|
221
|
+
export interface OverridesSection {
|
|
222
|
+
/**
|
|
223
|
+
* Every `flemo:*` storage key found (sessionStorage + localStorage),
|
|
224
|
+
* including unknown ones and keys that were present at attach but cleared
|
|
225
|
+
* since (marked in the key name). A non-empty map means this session does
|
|
226
|
+
* NOT run stock behavior.
|
|
227
|
+
*/
|
|
228
|
+
active: Record<string, string>;
|
|
229
|
+
/** Derived, prominent warnings — read these before trusting anything. */
|
|
230
|
+
warnings: string[];
|
|
231
|
+
}
|
|
232
|
+
export interface DriverPolicySection {
|
|
233
|
+
/**
|
|
234
|
+
* localStorage `flemo:motion-driver` — the learned ledger. "css" means the
|
|
235
|
+
* device earned a persisted player demotion; "raf" means a clean probe.
|
|
236
|
+
*/
|
|
237
|
+
demotion: string | null;
|
|
238
|
+
/**
|
|
239
|
+
* sessionStorage `flemo:motion-driver-force` raw value. NON-NULL MEANS A
|
|
240
|
+
* PIN IS ACTIVE: every transition is forced onto one driver. Almost always
|
|
241
|
+
* A/B residue when found unexpectedly.
|
|
242
|
+
*/
|
|
243
|
+
forcePin: string | null;
|
|
244
|
+
}
|
|
245
|
+
export interface FlemoReport {
|
|
246
|
+
generatedAt: string;
|
|
247
|
+
/** Report schema version (not the package version). */
|
|
248
|
+
version: string;
|
|
249
|
+
environment: EnvironmentFingerprint;
|
|
250
|
+
overrides: OverridesSection;
|
|
251
|
+
driverPolicy: DriverPolicySection;
|
|
252
|
+
flights: FlightRecord[];
|
|
253
|
+
/** Session-level findings (observation traps, active pins, stuck flights). */
|
|
254
|
+
anomalies: string[];
|
|
255
|
+
/**
|
|
256
|
+
* Constant list of layers NO in-page instrument can see. If every field in
|
|
257
|
+
* this report is clean and the user still sees jank, the cause lives in one
|
|
258
|
+
* of these — do not chase them with in-page tooling.
|
|
259
|
+
*/
|
|
260
|
+
blindSpots: string[];
|
|
261
|
+
/**
|
|
262
|
+
* Constant list of preconditions a motion verdict is only valid under (see
|
|
263
|
+
* judging.ts). The report cannot verify them from inside the page — an
|
|
264
|
+
* agent must confirm them with the user before trusting any judgement,
|
|
265
|
+
* including a clean one.
|
|
266
|
+
*/
|
|
267
|
+
judgingProtocol: string[];
|
|
268
|
+
}
|
|
269
|
+
export interface FlightRecorderOptions {
|
|
270
|
+
/** Ring-buffer size for recorded flights. Default 50. */
|
|
271
|
+
maxFlights?: number;
|
|
272
|
+
/** console.info a one-line summary per completed flight. Default false. */
|
|
273
|
+
log?: boolean;
|
|
274
|
+
/** Install window.flemo = { report, flights, detach }. Default true. */
|
|
275
|
+
installGlobal?: boolean;
|
|
276
|
+
}
|
|
277
|
+
export interface FlightRecorderHandle {
|
|
278
|
+
detach: () => void;
|
|
279
|
+
report: () => FlemoReport;
|
|
280
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flemo/devtools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config flight recorder for flemo screen transitions: captures per-flight driver routing, frame pacing, long tasks, landing residues, active debug overrides, and environment/observation-trap fingerprints into one JSON report readable by humans and coding agents.",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"keywords": [
|
|
14
|
+
"flemo",
|
|
15
|
+
"devtools",
|
|
16
|
+
"diagnostics",
|
|
17
|
+
"flight-recorder",
|
|
18
|
+
"transition",
|
|
19
|
+
"performance"
|
|
20
|
+
],
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "kimjh96",
|
|
23
|
+
"email": "kimjhs@kakao.com"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/kimjh96/flemo.git",
|
|
28
|
+
"directory": "packages/devtools"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/kimjh96/flemo/issues",
|
|
32
|
+
"email": "kimjhs@kakao.com"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://flemo.dev",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.13.1",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
39
|
+
"eslint": "^9.39.5",
|
|
40
|
+
"jsdom": "^29.1.1",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
42
|
+
"vite": "^8.2.1",
|
|
43
|
+
"vite-plugin-dts": "^5.0.3",
|
|
44
|
+
"vitest": "^4.1.10",
|
|
45
|
+
"@flemo/eslint-config": "0.0.0",
|
|
46
|
+
"@flemo/tsconfig": "0.0.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "vite build",
|
|
53
|
+
"watch": "vite build --watch",
|
|
54
|
+
"dev": "vite build --watch",
|
|
55
|
+
"lint": "eslint \"**/*.{js,mjs,ts,jsx,tsx,mts}\"",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"test:watch": "vitest",
|
|
60
|
+
"clean": "rm -rf dist .turbo"
|
|
61
|
+
}
|
|
62
|
+
}
|