@michaelmagan/dnumb 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/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/analysis/cluster.cjs +114 -0
- package/dist/analysis/cluster.d.cts +68 -0
- package/dist/analysis/cluster.d.ts +68 -0
- package/dist/analysis/cluster.js +109 -0
- package/dist/analysis/distances.cjs +124 -0
- package/dist/analysis/distances.d.cts +58 -0
- package/dist/analysis/distances.d.ts +58 -0
- package/dist/analysis/distances.js +119 -0
- package/dist/analysis/geometry.cjs +60 -0
- package/dist/analysis/geometry.d.cts +34 -0
- package/dist/analysis/geometry.d.ts +34 -0
- package/dist/analysis/geometry.js +53 -0
- package/dist/analysis/layout-tree.cjs +245 -0
- package/dist/analysis/layout-tree.d.cts +45 -0
- package/dist/analysis/layout-tree.d.ts +45 -0
- package/dist/analysis/layout-tree.js +242 -0
- package/dist/analysis/region-query.cjs +71 -0
- package/dist/analysis/region-query.d.cts +25 -0
- package/dist/analysis/region-query.d.ts +25 -0
- package/dist/analysis/region-query.js +67 -0
- package/dist/analysis/types.cjs +7 -0
- package/dist/analysis/types.d.cts +143 -0
- package/dist/analysis/types.d.ts +143 -0
- package/dist/analysis/types.js +6 -0
- package/dist/bundle.cjs +182 -0
- package/dist/bundle.d.cts +91 -0
- package/dist/bundle.d.ts +91 -0
- package/dist/bundle.js +175 -0
- package/dist/cli.cjs +755 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +719 -0
- package/dist/diff/changes.cjs +656 -0
- package/dist/diff/changes.d.cts +2 -0
- package/dist/diff/changes.d.ts +2 -0
- package/dist/diff/changes.js +653 -0
- package/dist/diff/identity.cjs +167 -0
- package/dist/diff/identity.d.cts +62 -0
- package/dist/diff/identity.d.ts +62 -0
- package/dist/diff/identity.js +159 -0
- package/dist/diff/index.cjs +84 -0
- package/dist/diff/index.d.cts +76 -0
- package/dist/diff/index.d.ts +76 -0
- package/dist/diff/index.js +68 -0
- package/dist/diff/match.cjs +156 -0
- package/dist/diff/match.d.cts +17 -0
- package/dist/diff/match.d.ts +17 -0
- package/dist/diff/match.js +152 -0
- package/dist/diff/render.cjs +540 -0
- package/dist/diff/render.d.cts +2 -0
- package/dist/diff/render.d.ts +2 -0
- package/dist/diff/render.js +537 -0
- package/dist/diff/types.cjs +2 -0
- package/dist/diff/types.d.cts +211 -0
- package/dist/diff/types.d.ts +211 -0
- package/dist/diff/types.js +1 -0
- package/dist/digest/index.cjs +8 -0
- package/dist/digest/index.d.cts +4 -0
- package/dist/digest/index.d.ts +4 -0
- package/dist/digest/index.js +2 -0
- package/dist/digest/parse.cjs +163 -0
- package/dist/digest/parse.d.cts +80 -0
- package/dist/digest/parse.d.ts +80 -0
- package/dist/digest/parse.js +160 -0
- package/dist/digest/render.cjs +1575 -0
- package/dist/digest/render.d.cts +41 -0
- package/dist/digest/render.d.ts +41 -0
- package/dist/digest/render.js +1571 -0
- package/dist/digest/types.cjs +10 -0
- package/dist/digest/types.d.cts +86 -0
- package/dist/digest/types.d.ts +86 -0
- package/dist/digest/types.js +9 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +19 -0
- package/dist/page.cjs +35 -0
- package/dist/page.d.cts +40 -0
- package/dist/page.d.ts +40 -0
- package/dist/page.js +31 -0
- package/dist/playwright.cjs +138 -0
- package/dist/playwright.d.cts +69 -0
- package/dist/playwright.d.ts +69 -0
- package/dist/playwright.js +131 -0
- package/dist/query.cjs +1152 -0
- package/dist/query.d.cts +47 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +1146 -0
- package/dist/snapshot/capture.cjs +80 -0
- package/dist/snapshot/capture.d.cts +34 -0
- package/dist/snapshot/capture.d.ts +34 -0
- package/dist/snapshot/capture.js +77 -0
- package/dist/snapshot/cdp.cjs +450 -0
- package/dist/snapshot/cdp.d.cts +36 -0
- package/dist/snapshot/cdp.d.ts +36 -0
- package/dist/snapshot/cdp.js +447 -0
- package/dist/snapshot/clip.cjs +100 -0
- package/dist/snapshot/clip.d.cts +26 -0
- package/dist/snapshot/clip.d.ts +26 -0
- package/dist/snapshot/clip.js +96 -0
- package/dist/snapshot/collect.cjs +358 -0
- package/dist/snapshot/collect.d.cts +18 -0
- package/dist/snapshot/collect.d.ts +18 -0
- package/dist/snapshot/collect.js +355 -0
- package/dist/snapshot/color.cjs +1039 -0
- package/dist/snapshot/color.d.cts +316 -0
- package/dist/snapshot/color.d.ts +316 -0
- package/dist/snapshot/color.js +1026 -0
- package/dist/snapshot/facts.cjs +167 -0
- package/dist/snapshot/facts.d.cts +48 -0
- package/dist/snapshot/facts.d.ts +48 -0
- package/dist/snapshot/facts.js +161 -0
- package/dist/snapshot/limits.cjs +69 -0
- package/dist/snapshot/limits.d.cts +59 -0
- package/dist/snapshot/limits.d.ts +59 -0
- package/dist/snapshot/limits.js +65 -0
- package/dist/snapshot/not-measured.cjs +44 -0
- package/dist/snapshot/not-measured.d.cts +14 -0
- package/dist/snapshot/not-measured.d.ts +14 -0
- package/dist/snapshot/not-measured.js +41 -0
- package/dist/snapshot/types.cjs +2 -0
- package/dist/snapshot/types.d.cts +238 -0
- package/dist/snapshot/types.d.ts +238 -0
- package/dist/snapshot/types.js +1 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +15 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.js +6 -0
- package/package.json +141 -0
- package/skills/dnumb/SKILL.md +136 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometry primitives. Deliberately structural and tree-unaware: every
|
|
3
|
+
* function in `geometry.ts` takes plain rects, so the collector, the digest,
|
|
4
|
+
* and the diff all share one arithmetic implementation rather than three.
|
|
5
|
+
*/
|
|
6
|
+
/** A CSS-pixel rectangle in viewport coordinates. */
|
|
7
|
+
export interface Rect {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
/** The four sides of a box property (`margin`, `padding`, `border`). */
|
|
14
|
+
export interface BoxSides {
|
|
15
|
+
top: number;
|
|
16
|
+
right: number;
|
|
17
|
+
bottom: number;
|
|
18
|
+
left: number;
|
|
19
|
+
}
|
|
20
|
+
export type Flow = 'row' | 'column' | 'grid' | 'none';
|
|
21
|
+
export type LayoutNodeKind = 'leaf' | 'container';
|
|
22
|
+
/**
|
|
23
|
+
* One node in the layout tree `buildLayoutTree` produces. `elementId`/`tag`/`rect`
|
|
24
|
+
* identify the *representative* underlying element — for a
|
|
25
|
+
* folded wrapper chain (see `wrapped`) that's the innermost element, since
|
|
26
|
+
* it carries the meaningful content (text, replaced-element type, or a
|
|
27
|
+
* distinct border/background); the outer wrapper's box is what proved the
|
|
28
|
+
* fold was safe (its content box matched), but it has nothing worth
|
|
29
|
+
* reporting on its own once absorbed.
|
|
30
|
+
*/
|
|
31
|
+
export interface LayoutNode {
|
|
32
|
+
elementId: string;
|
|
33
|
+
tag: string;
|
|
34
|
+
kind: LayoutNodeKind;
|
|
35
|
+
flow: Flow;
|
|
36
|
+
/** Computed CSS `position` of the representative element (`static`,
|
|
37
|
+
* `relative`, `absolute`, `fixed`, `sticky`) — carried through from
|
|
38
|
+
* `ElementRecord.position` so `Analysis`-level geometry (`overlaps`,
|
|
39
|
+
* `proximities`) can exclude `fixed`/`sticky` chrome the same way
|
|
40
|
+
* `checks/overflow.ts#UNCONTAINED_POSITIONS` already does at the
|
|
41
|
+
* `ElementRecord` level. */
|
|
42
|
+
position: string;
|
|
43
|
+
/** Count of wrapper levels folded into this node (0 if none). */
|
|
44
|
+
wrapped: number;
|
|
45
|
+
rect: Rect;
|
|
46
|
+
children: LayoutNode[];
|
|
47
|
+
}
|
|
48
|
+
export interface LayoutTreeDropped {
|
|
49
|
+
svgInternal: number;
|
|
50
|
+
invisible: number;
|
|
51
|
+
wrappers: number;
|
|
52
|
+
}
|
|
53
|
+
export interface LayoutTreeResult {
|
|
54
|
+
tree: LayoutNode[];
|
|
55
|
+
dropped: LayoutTreeDropped;
|
|
56
|
+
}
|
|
57
|
+
export interface SiblingGapPair {
|
|
58
|
+
a: string;
|
|
59
|
+
b: string;
|
|
60
|
+
gap: number;
|
|
61
|
+
}
|
|
62
|
+
export interface SiblingGapGroup {
|
|
63
|
+
containerId: string;
|
|
64
|
+
axis: 'x' | 'y';
|
|
65
|
+
pairs: SiblingGapPair[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `SiblingGapGroup` plus base-unit detection over the group's own gap
|
|
69
|
+
* values (`pairs[].gap`) — `null` unit means no candidate explained enough
|
|
70
|
+
* of the gaps (see `cluster.ts#detectBaseUnit`). This is what `Analysis.gaps`
|
|
71
|
+
* actually holds: extending `SiblingGapGroup` rather than a parallel array
|
|
72
|
+
* keyed by `containerId`, since every consumer that already has a gap group
|
|
73
|
+
* (the digest renderer, the future spacing check) needs the group's own
|
|
74
|
+
* spacing stats in the same place it reads `pairs`, and a parallel array
|
|
75
|
+
* would just be a second lookup by the same key for no benefit — see
|
|
76
|
+
* LOG.md step 9. `siblingGaps()` in `neighbors.ts` itself still returns the
|
|
77
|
+
* plain `SiblingGapGroup` (it has no candidates/tolerance to detect a base
|
|
78
|
+
* unit with); `analyze()` is what adds these fields on top.
|
|
79
|
+
*/
|
|
80
|
+
export interface SpacingGapGroup extends SiblingGapGroup {
|
|
81
|
+
baseUnit: number | null;
|
|
82
|
+
onGridShare: number;
|
|
83
|
+
offGrid: number[];
|
|
84
|
+
distinct: Record<string, number>;
|
|
85
|
+
}
|
|
86
|
+
export interface OverlapPair {
|
|
87
|
+
a: string;
|
|
88
|
+
b: string;
|
|
89
|
+
area: number;
|
|
90
|
+
rect: Rect;
|
|
91
|
+
}
|
|
92
|
+
export interface ProximityPair {
|
|
93
|
+
a: string;
|
|
94
|
+
b: string;
|
|
95
|
+
edge: number;
|
|
96
|
+
dx: number;
|
|
97
|
+
dy: number;
|
|
98
|
+
}
|
|
99
|
+
export interface EdgeClusterSet {
|
|
100
|
+
left: Array<{
|
|
101
|
+
value: number;
|
|
102
|
+
count: number;
|
|
103
|
+
members: number[];
|
|
104
|
+
}>;
|
|
105
|
+
right: Array<{
|
|
106
|
+
value: number;
|
|
107
|
+
count: number;
|
|
108
|
+
members: number[];
|
|
109
|
+
}>;
|
|
110
|
+
top: Array<{
|
|
111
|
+
value: number;
|
|
112
|
+
count: number;
|
|
113
|
+
members: number[];
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
export interface NearMiss {
|
|
117
|
+
id: string;
|
|
118
|
+
axis: 'left' | 'right' | 'top';
|
|
119
|
+
actual: number;
|
|
120
|
+
nearestClusterValue: number;
|
|
121
|
+
delta: number;
|
|
122
|
+
/** Count of `nearestClusterValue`'s members that are container-scoped to
|
|
123
|
+
* this node — nearest common ancestor no more than `index.ts`'s
|
|
124
|
+
* `MAX_ANCESTOR_DISTANCE` combined steps away — not the cluster's raw
|
|
125
|
+
* page-wide `count` in `Analysis.edges`. `checkAlignment`'s
|
|
126
|
+
* `minClusterSize` gate reads this field instead of looking up
|
|
127
|
+
* `Analysis.edges`, so an unrelated page-wide cluster (large `count`,
|
|
128
|
+
* but zero members actually related to this node) can't pass the gate
|
|
129
|
+
* on size alone. See LOG.md Step 11b. */
|
|
130
|
+
scopedMemberCount: number;
|
|
131
|
+
}
|
|
132
|
+
export interface UniformityDeviant {
|
|
133
|
+
id: string;
|
|
134
|
+
reason: string;
|
|
135
|
+
}
|
|
136
|
+
export interface SiblingUniformityGroup {
|
|
137
|
+
containerId: string;
|
|
138
|
+
children: number;
|
|
139
|
+
medianWidth: number;
|
|
140
|
+
medianHeight: number;
|
|
141
|
+
medianGap: number;
|
|
142
|
+
deviants: UniformityDeviant[];
|
|
143
|
+
}
|
package/dist/bundle.cjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BundleFormatError = void 0;
|
|
4
|
+
exports.buildBundle = buildBundle;
|
|
5
|
+
exports.parseBundle = parseBundle;
|
|
6
|
+
exports.serializeBundle = serializeBundle;
|
|
7
|
+
const index_js_1 = require("./spec/index.cjs");
|
|
8
|
+
/**
|
|
9
|
+
* The four color-capture string fields and the two overlap-signal fields are
|
|
10
|
+
* required on `ElementRecord` (`snapshot/types.ts`) -- both real capture
|
|
11
|
+
* paths always set all six, the same as `rect` or `overflowX`. A bundle
|
|
12
|
+
* written before they existed simply has no key for them at all in its JSON,
|
|
13
|
+
* though, which is not the same violation as a caller hand-building a record
|
|
14
|
+
* and skipping one: this is a format-migration problem, not a construction
|
|
15
|
+
* error, so it gets a format-migration fix here rather than the clear thrown
|
|
16
|
+
* error `snapshot/color.ts#requireColorField` gives a hand-built record that
|
|
17
|
+
* is missing a required field. Reading `undefined.trim()` or
|
|
18
|
+
* `undefined.indexOf(...)` off an unmigrated old record would throw deep
|
|
19
|
+
* inside `snapshot/color.ts` regardless of which of the two situations
|
|
20
|
+
* produced it, which is exactly the failure this function exists to
|
|
21
|
+
* pre-empt for the bundle case specifically.
|
|
22
|
+
*
|
|
23
|
+
* `textShadow`/`textStrokeWidth`/`textStrokeColor` are backfilled with the
|
|
24
|
+
* same defaults a fresh capture with nothing to report would itself have
|
|
25
|
+
* written -- `'none'`/`'0px'`/opaque black -- because a missing value and a
|
|
26
|
+
* genuine "no such style" both render identically for these three (each is
|
|
27
|
+
* an optional counter-fact token, printed only when present; see
|
|
28
|
+
* `snapshot/color.ts#colorFactFor`'s tail tokens), so there is no
|
|
29
|
+
* fabrication risk in picking the harmless default.
|
|
30
|
+
*
|
|
31
|
+
* `backgroundImage` does NOT get that treatment (A3, fixed here): it feeds
|
|
32
|
+
* `resolveBackdrop`'s gradient/image detection directly, so defaulting a
|
|
33
|
+
* genuinely-uncaptured value to `'none'` used to manufacture the positive
|
|
34
|
+
* claim "this element has no background image" -- which then silently
|
|
35
|
+
* terminated the compositing walk and printed a specific, fabricated ratio.
|
|
36
|
+
* `null` here means exactly what it means on `hitStackIds`/`paintOrder`
|
|
37
|
+
* below: this specific capture never measured this field, and
|
|
38
|
+
* `snapshot/color.ts` refuses to guess past it (reports
|
|
39
|
+
* `unmeasured (not-captured ...)` instead) -- the same "backfill the honest
|
|
40
|
+
* unknown, not a false positive" discipline `NO_CAPTURE_NOT_MEASURED` below
|
|
41
|
+
* uses for shadow roots and iframes.
|
|
42
|
+
*
|
|
43
|
+
* Every record `parseBundle` returns satisfies the required contract by
|
|
44
|
+
* construction; `requireColorField` exists for the different case of a
|
|
45
|
+
* record that never went through this function at all.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeElement(raw) {
|
|
48
|
+
const el = raw;
|
|
49
|
+
return {
|
|
50
|
+
...el,
|
|
51
|
+
backgroundImage: el.backgroundImage ?? null,
|
|
52
|
+
textShadow: el.textShadow ?? 'none',
|
|
53
|
+
textStrokeWidth: el.textStrokeWidth ?? '0px',
|
|
54
|
+
textStrokeColor: el.textStrokeColor ?? 'rgb(0, 0, 0)',
|
|
55
|
+
hitStackIds: el.hitStackIds ?? null,
|
|
56
|
+
paintOrder: el.paintOrder ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Stand-in for `notMeasured` when a `CaptureResult` has none -- both real
|
|
61
|
+
* capture paths always set it, so this only fires for a hand-built result
|
|
62
|
+
* that skipped them entirely (a test fixture, typically). `closed: 'unknown'`
|
|
63
|
+
* because that is true regardless of how the result was built; the rest
|
|
64
|
+
* default to zero rather than to a matching 'unknown', which is a real
|
|
65
|
+
* "no measurement was taken" gap this constant papers over for a
|
|
66
|
+
* non-production caller, not a claim about the page.
|
|
67
|
+
*/
|
|
68
|
+
const NO_CAPTURE_NOT_MEASURED = {
|
|
69
|
+
shadowRoots: { open: 0, closed: 'unknown' },
|
|
70
|
+
iframes: { sameOrigin: 0, crossOrigin: 0, unknownOrigin: 0 },
|
|
71
|
+
};
|
|
72
|
+
function buildBundle(capture, options) {
|
|
73
|
+
return {
|
|
74
|
+
spec: index_js_1.SPEC_VERSION,
|
|
75
|
+
name: options.name,
|
|
76
|
+
url: options.url,
|
|
77
|
+
capturedAt: options.capturedAt,
|
|
78
|
+
viewport: options.viewportName
|
|
79
|
+
? { name: options.viewportName, ...capture.viewport }
|
|
80
|
+
: capture.viewport,
|
|
81
|
+
strategy: capture.strategy,
|
|
82
|
+
truncated: capture.truncated,
|
|
83
|
+
...(capture.cdpFellBack ? { cdpFellBack: true } : {}),
|
|
84
|
+
notMeasured: capture.notMeasured ?? NO_CAPTURE_NOT_MEASURED,
|
|
85
|
+
documentBackground: capture.documentBackground ?? null,
|
|
86
|
+
elements: capture.elements,
|
|
87
|
+
...(options.labels ? { labels: options.labels } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
class BundleFormatError extends Error {
|
|
91
|
+
name = 'BundleFormatError';
|
|
92
|
+
}
|
|
93
|
+
exports.BundleFormatError = BundleFormatError;
|
|
94
|
+
/**
|
|
95
|
+
* Parse and validate. Structural, not schema-library: the check exists so a
|
|
96
|
+
* consumer gets "this is not a dnumb bundle" rather than `undefined is not an
|
|
97
|
+
* object` fourteen frames deep, and that does not need a dependency.
|
|
98
|
+
*/
|
|
99
|
+
function parseBundle(text, source = '<input>') {
|
|
100
|
+
let value;
|
|
101
|
+
try {
|
|
102
|
+
value = JSON.parse(text);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new BundleFormatError(`${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
if (typeof value !== 'object' || value === null) {
|
|
108
|
+
throw new BundleFormatError(`${source} is not a dnumb bundle (expected a JSON object)`);
|
|
109
|
+
}
|
|
110
|
+
const bundle = value;
|
|
111
|
+
if (typeof bundle.spec !== 'number') {
|
|
112
|
+
throw new BundleFormatError(`${source} is not a dnumb bundle (no "spec" field)`);
|
|
113
|
+
}
|
|
114
|
+
if (bundle.spec > index_js_1.SPEC_VERSION) {
|
|
115
|
+
throw new BundleFormatError(`${source} was written by a newer dnumb (spec ${bundle.spec}; this build reads ${index_js_1.SPEC_VERSION}). Upgrade dnumb.`);
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(bundle.elements)) {
|
|
118
|
+
throw new BundleFormatError(`${source} has no "elements" array`);
|
|
119
|
+
}
|
|
120
|
+
if (typeof bundle.viewport !== 'object' || bundle.viewport === null) {
|
|
121
|
+
throw new BundleFormatError(`${source} has no "viewport"`);
|
|
122
|
+
}
|
|
123
|
+
// `Bundle.strategy`'s own doc comment: "Recorded, never inferred" (B4).
|
|
124
|
+
// `bundle.strategy ?? 'walk'` used to assert a specific capture path a
|
|
125
|
+
// bundle never actually recorded -- a `cdp` bundle whose `"strategy"` key
|
|
126
|
+
// happened to be missing (an old writer, a hand-edited file) would parse
|
|
127
|
+
// back claiming to be `walk`, which is a different, real fact this bundle
|
|
128
|
+
// never measured. Neither value is an honest default for "don't know", so
|
|
129
|
+
// this refuses to parse rather than guess one -- the same register the
|
|
130
|
+
// `notMeasured` check just below uses for the identical shape of problem.
|
|
131
|
+
if (bundle.strategy !== 'cdp' && bundle.strategy !== 'walk') {
|
|
132
|
+
throw new BundleFormatError(`${source} has no valid "strategy" (must be "cdp" or "walk", got ` +
|
|
133
|
+
`${JSON.stringify(bundle.strategy)}): this build cannot honestly say which capture path ` +
|
|
134
|
+
'produced these records, and defaulting to "walk" would assert a specific path this ' +
|
|
135
|
+
'bundle never recorded. Re-capture the page with a current dnumb.');
|
|
136
|
+
}
|
|
137
|
+
// `NotMeasured`'s own doc comment: "every other field here is always a
|
|
138
|
+
// real, counted number." `NO_CAPTURE_NOT_MEASURED`'s zeroed shape is that
|
|
139
|
+
// promise's one sanctioned exception, and it is scoped narrowly -- "a
|
|
140
|
+
// hand-built [CaptureResult] that skipped them entirely (a test fixture,
|
|
141
|
+
// typically)", i.e. `buildBundle`'s in-memory case just above, where no
|
|
142
|
+
// capture happened at all and nothing claims otherwise. A bundle read back
|
|
143
|
+
// from disk is different: it is a *real* prior capture, and reusing that
|
|
144
|
+
// same all-zero shape for one that predates `notMeasured` used to print a
|
|
145
|
+
// specific, confident zero cross-origin-iframe count a page might not
|
|
146
|
+
// actually have (B4) -- exactly the "always a real, counted number"
|
|
147
|
+
// promise broken. There is no honest partial value this format can hand
|
|
148
|
+
// back for that case (unlike `strategy`, `NotMeasured`'s numeric fields
|
|
149
|
+
// have no `| undefined`/`'unknown'` escape hatch), so this refuses outright
|
|
150
|
+
// rather than fabricate one: re-capture with a current dnumb instead.
|
|
151
|
+
if (bundle.notMeasured === undefined) {
|
|
152
|
+
throw new BundleFormatError(`${source} predates capture accounting ("notMeasured") and cannot be read by this build: ` +
|
|
153
|
+
'it has no honest way to report shadow-DOM/iframe coverage for a bundle this old, and ' +
|
|
154
|
+
'silently reporting zero would claim a specific count this bundle never measured. ' +
|
|
155
|
+
're-capture the page with a current dnumb to get a bundle this build can read.');
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
// The value this bundle actually claims, preserved -- never rewritten to
|
|
159
|
+
// this build's own `SPEC_VERSION` (B4). Already validated above: a
|
|
160
|
+
// number, and no greater than `SPEC_VERSION`.
|
|
161
|
+
spec: bundle.spec,
|
|
162
|
+
name: bundle.name ?? 'capture',
|
|
163
|
+
url: bundle.url ?? '',
|
|
164
|
+
capturedAt: bundle.capturedAt ?? '',
|
|
165
|
+
viewport: bundle.viewport,
|
|
166
|
+
// Already validated above: exactly `'cdp'` or `'walk'`, never inferred.
|
|
167
|
+
strategy: bundle.strategy,
|
|
168
|
+
truncated: bundle.truncated ?? false,
|
|
169
|
+
...(bundle.cdpFellBack ? { cdpFellBack: true } : {}),
|
|
170
|
+
notMeasured: bundle.notMeasured,
|
|
171
|
+
// `null` on a bundle old enough to predate color capture entirely (before
|
|
172
|
+
// `documentBackground` existed) -- an honest "never measured", not a
|
|
173
|
+
// guessed canvas color. See `DocumentBackground`.
|
|
174
|
+
documentBackground: bundle.documentBackground ?? null,
|
|
175
|
+
elements: bundle.elements.map(normalizeElement),
|
|
176
|
+
...(bundle.labels ? { labels: bundle.labels } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** The bundle as it is written to disk. Stable key order, one trailing newline. */
|
|
180
|
+
function serializeBundle(bundle) {
|
|
181
|
+
return `${JSON.stringify(bundle, null, 2)}\n`;
|
|
182
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture bundle — the load-bearing object in this package.
|
|
3
|
+
*
|
|
4
|
+
* Not the browser session. A bundle is a self-describing file that answers
|
|
5
|
+
* `q describe e91` six months later, on a machine with no app and no browser.
|
|
6
|
+
* Percy and Chromatic artifacts are pixels for human eyes; an aria snapshot has
|
|
7
|
+
* no geometry; a Playwright trace is a replayable page rather than a set of
|
|
8
|
+
* measurements. This is measurements, and it travels.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately a plain JSON object with a version on it, because the
|
|
11
|
+
* thing most likely to read it is a program written after this one.
|
|
12
|
+
*/
|
|
13
|
+
import type { CaptureResult } from "./snapshot/capture.cjs";
|
|
14
|
+
import type { DocumentBackground, ElementRecord, NotMeasured } from "./snapshot/types.cjs";
|
|
15
|
+
export interface Bundle {
|
|
16
|
+
/**
|
|
17
|
+
* The capture format version this bundle actually claims to be.
|
|
18
|
+
*
|
|
19
|
+
* A fresh capture (`buildBundle`) always writes the running build's own
|
|
20
|
+
* `SPEC_VERSION`, because that is genuinely what it is. Reading a bundle
|
|
21
|
+
* back (`parseBundle`) is different: this is the value *parsed from the
|
|
22
|
+
* file*, preserved as-is, never silently rewritten to the current build's
|
|
23
|
+
* `SPEC_VERSION` (B4) -- doing that used to make a bundle from an older
|
|
24
|
+
* spec claim, falsely, to be current. It can therefore be lower than
|
|
25
|
+
* `SPEC_VERSION` (never higher: `parseBundle` already throws on that, see
|
|
26
|
+
* below) on a bundle this build can still read but did not itself write.
|
|
27
|
+
*/
|
|
28
|
+
spec: number;
|
|
29
|
+
/** Caller-chosen name for this capture: `settings-desktop`, `menu-open`. */
|
|
30
|
+
name: string;
|
|
31
|
+
url: string;
|
|
32
|
+
/** ISO 8601. Set by the writer, not by the collector. */
|
|
33
|
+
capturedAt: string;
|
|
34
|
+
viewport: {
|
|
35
|
+
name?: string;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Which capture path produced these records. Recorded, never inferred
|
|
41
|
+
* (B4): a bundle whose `"strategy"` is missing or not one of these two
|
|
42
|
+
* literals is not a bundle `parseBundle` can honestly assign a strategy
|
|
43
|
+
* to -- it refuses to parse rather than default to `'walk'`, which used
|
|
44
|
+
* to assert a specific capture path a bundle never recorded and could
|
|
45
|
+
* just as well have been `'cdp'`. See `parseBundle`.
|
|
46
|
+
*/
|
|
47
|
+
strategy: 'cdp' | 'walk';
|
|
48
|
+
/** True when the capture stopped at the element cap. */
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True only for a `strategy: 'walk'` bundle that got there by CDP failing
|
|
52
|
+
* on a Chromium page, not by running on a browser CDP never covers. See the
|
|
53
|
+
* doc comment on `CaptureResult.cdpFellBack`.
|
|
54
|
+
*/
|
|
55
|
+
cdpFellBack?: boolean;
|
|
56
|
+
/** Shadow DOM and iframe content this capture walked past. See `NotMeasured`. */
|
|
57
|
+
notMeasured: NotMeasured;
|
|
58
|
+
/**
|
|
59
|
+
* `<html>`'s own background-color/color-scheme -- see `DocumentBackground`.
|
|
60
|
+
* `null` on a bundle old enough to predate color capture entirely: an
|
|
61
|
+
* honest "never measured", the same register `documentBackground: null`
|
|
62
|
+
* uses everywhere else in this file, never a guessed color. Optional only
|
|
63
|
+
* so a hand-built `Bundle` in a test need not name a value it has no
|
|
64
|
+
* opinion about, exactly like `Snapshot.documentBackground` -- `buildBundle`
|
|
65
|
+
* and `parseBundle` both always set it (to a real value or `null`, never
|
|
66
|
+
* leaving the key absent).
|
|
67
|
+
*/
|
|
68
|
+
documentBackground?: DocumentBackground | null;
|
|
69
|
+
elements: ElementRecord[];
|
|
70
|
+
/** Free-form labels a caller attaches: theme, branch, test title. */
|
|
71
|
+
labels?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
export interface BuildBundleOptions {
|
|
74
|
+
name: string;
|
|
75
|
+
url: string;
|
|
76
|
+
capturedAt: string;
|
|
77
|
+
viewportName?: string;
|
|
78
|
+
labels?: Record<string, string>;
|
|
79
|
+
}
|
|
80
|
+
export declare function buildBundle(capture: CaptureResult, options: BuildBundleOptions): Bundle;
|
|
81
|
+
export declare class BundleFormatError extends Error {
|
|
82
|
+
name: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse and validate. Structural, not schema-library: the check exists so a
|
|
86
|
+
* consumer gets "this is not a dnumb bundle" rather than `undefined is not an
|
|
87
|
+
* object` fourteen frames deep, and that does not need a dependency.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseBundle(text: string, source?: string): Bundle;
|
|
90
|
+
/** The bundle as it is written to disk. Stable key order, one trailing newline. */
|
|
91
|
+
export declare function serializeBundle(bundle: Bundle): string;
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture bundle — the load-bearing object in this package.
|
|
3
|
+
*
|
|
4
|
+
* Not the browser session. A bundle is a self-describing file that answers
|
|
5
|
+
* `q describe e91` six months later, on a machine with no app and no browser.
|
|
6
|
+
* Percy and Chromatic artifacts are pixels for human eyes; an aria snapshot has
|
|
7
|
+
* no geometry; a Playwright trace is a replayable page rather than a set of
|
|
8
|
+
* measurements. This is measurements, and it travels.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately a plain JSON object with a version on it, because the
|
|
11
|
+
* thing most likely to read it is a program written after this one.
|
|
12
|
+
*/
|
|
13
|
+
import type { CaptureResult } from "./snapshot/capture.js";
|
|
14
|
+
import type { DocumentBackground, ElementRecord, NotMeasured } from "./snapshot/types.js";
|
|
15
|
+
export interface Bundle {
|
|
16
|
+
/**
|
|
17
|
+
* The capture format version this bundle actually claims to be.
|
|
18
|
+
*
|
|
19
|
+
* A fresh capture (`buildBundle`) always writes the running build's own
|
|
20
|
+
* `SPEC_VERSION`, because that is genuinely what it is. Reading a bundle
|
|
21
|
+
* back (`parseBundle`) is different: this is the value *parsed from the
|
|
22
|
+
* file*, preserved as-is, never silently rewritten to the current build's
|
|
23
|
+
* `SPEC_VERSION` (B4) -- doing that used to make a bundle from an older
|
|
24
|
+
* spec claim, falsely, to be current. It can therefore be lower than
|
|
25
|
+
* `SPEC_VERSION` (never higher: `parseBundle` already throws on that, see
|
|
26
|
+
* below) on a bundle this build can still read but did not itself write.
|
|
27
|
+
*/
|
|
28
|
+
spec: number;
|
|
29
|
+
/** Caller-chosen name for this capture: `settings-desktop`, `menu-open`. */
|
|
30
|
+
name: string;
|
|
31
|
+
url: string;
|
|
32
|
+
/** ISO 8601. Set by the writer, not by the collector. */
|
|
33
|
+
capturedAt: string;
|
|
34
|
+
viewport: {
|
|
35
|
+
name?: string;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Which capture path produced these records. Recorded, never inferred
|
|
41
|
+
* (B4): a bundle whose `"strategy"` is missing or not one of these two
|
|
42
|
+
* literals is not a bundle `parseBundle` can honestly assign a strategy
|
|
43
|
+
* to -- it refuses to parse rather than default to `'walk'`, which used
|
|
44
|
+
* to assert a specific capture path a bundle never recorded and could
|
|
45
|
+
* just as well have been `'cdp'`. See `parseBundle`.
|
|
46
|
+
*/
|
|
47
|
+
strategy: 'cdp' | 'walk';
|
|
48
|
+
/** True when the capture stopped at the element cap. */
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* True only for a `strategy: 'walk'` bundle that got there by CDP failing
|
|
52
|
+
* on a Chromium page, not by running on a browser CDP never covers. See the
|
|
53
|
+
* doc comment on `CaptureResult.cdpFellBack`.
|
|
54
|
+
*/
|
|
55
|
+
cdpFellBack?: boolean;
|
|
56
|
+
/** Shadow DOM and iframe content this capture walked past. See `NotMeasured`. */
|
|
57
|
+
notMeasured: NotMeasured;
|
|
58
|
+
/**
|
|
59
|
+
* `<html>`'s own background-color/color-scheme -- see `DocumentBackground`.
|
|
60
|
+
* `null` on a bundle old enough to predate color capture entirely: an
|
|
61
|
+
* honest "never measured", the same register `documentBackground: null`
|
|
62
|
+
* uses everywhere else in this file, never a guessed color. Optional only
|
|
63
|
+
* so a hand-built `Bundle` in a test need not name a value it has no
|
|
64
|
+
* opinion about, exactly like `Snapshot.documentBackground` -- `buildBundle`
|
|
65
|
+
* and `parseBundle` both always set it (to a real value or `null`, never
|
|
66
|
+
* leaving the key absent).
|
|
67
|
+
*/
|
|
68
|
+
documentBackground?: DocumentBackground | null;
|
|
69
|
+
elements: ElementRecord[];
|
|
70
|
+
/** Free-form labels a caller attaches: theme, branch, test title. */
|
|
71
|
+
labels?: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
export interface BuildBundleOptions {
|
|
74
|
+
name: string;
|
|
75
|
+
url: string;
|
|
76
|
+
capturedAt: string;
|
|
77
|
+
viewportName?: string;
|
|
78
|
+
labels?: Record<string, string>;
|
|
79
|
+
}
|
|
80
|
+
export declare function buildBundle(capture: CaptureResult, options: BuildBundleOptions): Bundle;
|
|
81
|
+
export declare class BundleFormatError extends Error {
|
|
82
|
+
name: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse and validate. Structural, not schema-library: the check exists so a
|
|
86
|
+
* consumer gets "this is not a dnumb bundle" rather than `undefined is not an
|
|
87
|
+
* object` fourteen frames deep, and that does not need a dependency.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseBundle(text: string, source?: string): Bundle;
|
|
90
|
+
/** The bundle as it is written to disk. Stable key order, one trailing newline. */
|
|
91
|
+
export declare function serializeBundle(bundle: Bundle): string;
|