@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.
Files changed (133) hide show
  1. package/AGENTS.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +297 -0
  4. package/dist/analysis/cluster.cjs +114 -0
  5. package/dist/analysis/cluster.d.cts +68 -0
  6. package/dist/analysis/cluster.d.ts +68 -0
  7. package/dist/analysis/cluster.js +109 -0
  8. package/dist/analysis/distances.cjs +124 -0
  9. package/dist/analysis/distances.d.cts +58 -0
  10. package/dist/analysis/distances.d.ts +58 -0
  11. package/dist/analysis/distances.js +119 -0
  12. package/dist/analysis/geometry.cjs +60 -0
  13. package/dist/analysis/geometry.d.cts +34 -0
  14. package/dist/analysis/geometry.d.ts +34 -0
  15. package/dist/analysis/geometry.js +53 -0
  16. package/dist/analysis/layout-tree.cjs +245 -0
  17. package/dist/analysis/layout-tree.d.cts +45 -0
  18. package/dist/analysis/layout-tree.d.ts +45 -0
  19. package/dist/analysis/layout-tree.js +242 -0
  20. package/dist/analysis/region-query.cjs +71 -0
  21. package/dist/analysis/region-query.d.cts +25 -0
  22. package/dist/analysis/region-query.d.ts +25 -0
  23. package/dist/analysis/region-query.js +67 -0
  24. package/dist/analysis/types.cjs +7 -0
  25. package/dist/analysis/types.d.cts +143 -0
  26. package/dist/analysis/types.d.ts +143 -0
  27. package/dist/analysis/types.js +6 -0
  28. package/dist/bundle.cjs +182 -0
  29. package/dist/bundle.d.cts +91 -0
  30. package/dist/bundle.d.ts +91 -0
  31. package/dist/bundle.js +175 -0
  32. package/dist/cli.cjs +755 -0
  33. package/dist/cli.d.cts +2 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +719 -0
  36. package/dist/diff/changes.cjs +656 -0
  37. package/dist/diff/changes.d.cts +2 -0
  38. package/dist/diff/changes.d.ts +2 -0
  39. package/dist/diff/changes.js +653 -0
  40. package/dist/diff/identity.cjs +167 -0
  41. package/dist/diff/identity.d.cts +62 -0
  42. package/dist/diff/identity.d.ts +62 -0
  43. package/dist/diff/identity.js +159 -0
  44. package/dist/diff/index.cjs +84 -0
  45. package/dist/diff/index.d.cts +76 -0
  46. package/dist/diff/index.d.ts +76 -0
  47. package/dist/diff/index.js +68 -0
  48. package/dist/diff/match.cjs +156 -0
  49. package/dist/diff/match.d.cts +17 -0
  50. package/dist/diff/match.d.ts +17 -0
  51. package/dist/diff/match.js +152 -0
  52. package/dist/diff/render.cjs +540 -0
  53. package/dist/diff/render.d.cts +2 -0
  54. package/dist/diff/render.d.ts +2 -0
  55. package/dist/diff/render.js +537 -0
  56. package/dist/diff/types.cjs +2 -0
  57. package/dist/diff/types.d.cts +211 -0
  58. package/dist/diff/types.d.ts +211 -0
  59. package/dist/diff/types.js +1 -0
  60. package/dist/digest/index.cjs +8 -0
  61. package/dist/digest/index.d.cts +4 -0
  62. package/dist/digest/index.d.ts +4 -0
  63. package/dist/digest/index.js +2 -0
  64. package/dist/digest/parse.cjs +163 -0
  65. package/dist/digest/parse.d.cts +80 -0
  66. package/dist/digest/parse.d.ts +80 -0
  67. package/dist/digest/parse.js +160 -0
  68. package/dist/digest/render.cjs +1575 -0
  69. package/dist/digest/render.d.cts +41 -0
  70. package/dist/digest/render.d.ts +41 -0
  71. package/dist/digest/render.js +1571 -0
  72. package/dist/digest/types.cjs +10 -0
  73. package/dist/digest/types.d.cts +86 -0
  74. package/dist/digest/types.d.ts +86 -0
  75. package/dist/digest/types.js +9 -0
  76. package/dist/index.cjs +61 -0
  77. package/dist/index.d.cts +27 -0
  78. package/dist/index.d.ts +27 -0
  79. package/dist/index.js +19 -0
  80. package/dist/page.cjs +35 -0
  81. package/dist/page.d.cts +40 -0
  82. package/dist/page.d.ts +40 -0
  83. package/dist/page.js +31 -0
  84. package/dist/playwright.cjs +138 -0
  85. package/dist/playwright.d.cts +69 -0
  86. package/dist/playwright.d.ts +69 -0
  87. package/dist/playwright.js +131 -0
  88. package/dist/query.cjs +1152 -0
  89. package/dist/query.d.cts +47 -0
  90. package/dist/query.d.ts +47 -0
  91. package/dist/query.js +1146 -0
  92. package/dist/snapshot/capture.cjs +80 -0
  93. package/dist/snapshot/capture.d.cts +34 -0
  94. package/dist/snapshot/capture.d.ts +34 -0
  95. package/dist/snapshot/capture.js +77 -0
  96. package/dist/snapshot/cdp.cjs +450 -0
  97. package/dist/snapshot/cdp.d.cts +36 -0
  98. package/dist/snapshot/cdp.d.ts +36 -0
  99. package/dist/snapshot/cdp.js +447 -0
  100. package/dist/snapshot/clip.cjs +100 -0
  101. package/dist/snapshot/clip.d.cts +26 -0
  102. package/dist/snapshot/clip.d.ts +26 -0
  103. package/dist/snapshot/clip.js +96 -0
  104. package/dist/snapshot/collect.cjs +358 -0
  105. package/dist/snapshot/collect.d.cts +18 -0
  106. package/dist/snapshot/collect.d.ts +18 -0
  107. package/dist/snapshot/collect.js +355 -0
  108. package/dist/snapshot/color.cjs +1039 -0
  109. package/dist/snapshot/color.d.cts +316 -0
  110. package/dist/snapshot/color.d.ts +316 -0
  111. package/dist/snapshot/color.js +1026 -0
  112. package/dist/snapshot/facts.cjs +167 -0
  113. package/dist/snapshot/facts.d.cts +48 -0
  114. package/dist/snapshot/facts.d.ts +48 -0
  115. package/dist/snapshot/facts.js +161 -0
  116. package/dist/snapshot/limits.cjs +69 -0
  117. package/dist/snapshot/limits.d.cts +59 -0
  118. package/dist/snapshot/limits.d.ts +59 -0
  119. package/dist/snapshot/limits.js +65 -0
  120. package/dist/snapshot/not-measured.cjs +44 -0
  121. package/dist/snapshot/not-measured.d.cts +14 -0
  122. package/dist/snapshot/not-measured.d.ts +14 -0
  123. package/dist/snapshot/not-measured.js +41 -0
  124. package/dist/snapshot/types.cjs +2 -0
  125. package/dist/snapshot/types.d.cts +238 -0
  126. package/dist/snapshot/types.d.ts +238 -0
  127. package/dist/snapshot/types.js +1 -0
  128. package/dist/spec/index.cjs +9 -0
  129. package/dist/spec/index.d.cts +15 -0
  130. package/dist/spec/index.d.ts +15 -0
  131. package/dist/spec/index.js +6 -0
  132. package/package.json +141 -0
  133. 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
+ }
@@ -0,0 +1,6 @@
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
+ export {};
@@ -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;
@@ -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;