@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,34 @@
1
+ /**
2
+ * Pure two-rect primitives — no tree awareness, no `ElementRecord`. Every
3
+ * function here takes plain `Rect`s so the collector, the digest, and the
4
+ * state diff can all share the exact same arithmetic.
5
+ *
6
+ * Ported unchanged from the prototype (`packages/layout-audit/src/analysis/
7
+ * geometry.ts`), where it was the best-tested pure module in the package.
8
+ */
9
+ import type { Rect } from "./types.cjs";
10
+ /**
11
+ * The overlapping rectangle between `a` and `b`, or `null` if they don't
12
+ * overlap on both axes. Touching edges count as no overlap — the intersection
13
+ * would have zero area, and a zero-area rect is not a thing anything on screen
14
+ * can be inside of.
15
+ */
16
+ export declare function intersect(a: Rect, b: Rect): Rect | null;
17
+ /** True if `inner` is fully within `outer`. Edges touching counts as contained. */
18
+ export declare function contains(outer: Rect, inner: Rect): boolean;
19
+ /**
20
+ * Signed edge-to-edge distance between `a` and `b` along one axis — positive is
21
+ * a real gap, negative is how far the two rects overlap along that axis.
22
+ *
23
+ * Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
24
+ * on `y` and have a positive gap on `x`, which is exactly the question a gutter
25
+ * measurement asks.
26
+ */
27
+ export declare function gapAlong(a: Rect, b: Rect, axis: 'x' | 'y'): number;
28
+ /** Rect area in px². */
29
+ export declare function area(r: Rect): number;
30
+ /** Rect center point. */
31
+ export declare function center(r: Rect): {
32
+ x: number;
33
+ y: number;
34
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure two-rect primitives — no tree awareness, no `ElementRecord`. Every
3
+ * function here takes plain `Rect`s so the collector, the digest, and the
4
+ * state diff can all share the exact same arithmetic.
5
+ *
6
+ * Ported unchanged from the prototype (`packages/layout-audit/src/analysis/
7
+ * geometry.ts`), where it was the best-tested pure module in the package.
8
+ */
9
+ import type { Rect } from "./types.js";
10
+ /**
11
+ * The overlapping rectangle between `a` and `b`, or `null` if they don't
12
+ * overlap on both axes. Touching edges count as no overlap — the intersection
13
+ * would have zero area, and a zero-area rect is not a thing anything on screen
14
+ * can be inside of.
15
+ */
16
+ export declare function intersect(a: Rect, b: Rect): Rect | null;
17
+ /** True if `inner` is fully within `outer`. Edges touching counts as contained. */
18
+ export declare function contains(outer: Rect, inner: Rect): boolean;
19
+ /**
20
+ * Signed edge-to-edge distance between `a` and `b` along one axis — positive is
21
+ * a real gap, negative is how far the two rects overlap along that axis.
22
+ *
23
+ * Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
24
+ * on `y` and have a positive gap on `x`, which is exactly the question a gutter
25
+ * measurement asks.
26
+ */
27
+ export declare function gapAlong(a: Rect, b: Rect, axis: 'x' | 'y'): number;
28
+ /** Rect area in px². */
29
+ export declare function area(r: Rect): number;
30
+ /** Rect center point. */
31
+ export declare function center(r: Rect): {
32
+ x: number;
33
+ y: number;
34
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The overlapping rectangle between `a` and `b`, or `null` if they don't
3
+ * overlap on both axes. Touching edges count as no overlap — the intersection
4
+ * would have zero area, and a zero-area rect is not a thing anything on screen
5
+ * can be inside of.
6
+ */
7
+ export function intersect(a, b) {
8
+ const x = Math.max(a.x, b.x);
9
+ const y = Math.max(a.y, b.y);
10
+ const right = Math.min(a.x + a.width, b.x + b.width);
11
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
12
+ const width = right - x;
13
+ const height = bottom - y;
14
+ if (width <= 0 || height <= 0)
15
+ return null;
16
+ return { x, y, width, height };
17
+ }
18
+ /** True if `inner` is fully within `outer`. Edges touching counts as contained. */
19
+ export function contains(outer, inner) {
20
+ return (inner.x >= outer.x &&
21
+ inner.y >= outer.y &&
22
+ inner.x + inner.width <= outer.x + outer.width &&
23
+ inner.y + inner.height <= outer.y + outer.height);
24
+ }
25
+ /**
26
+ * Signed edge-to-edge distance between `a` and `b` along one axis — positive is
27
+ * a real gap, negative is how far the two rects overlap along that axis.
28
+ *
29
+ * Unlike `intersect`, this is per-axis: two rects side by side in a row overlap
30
+ * on `y` and have a positive gap on `x`, which is exactly the question a gutter
31
+ * measurement asks.
32
+ */
33
+ export function gapAlong(a, b, axis) {
34
+ const size = axis === 'x' ? 'width' : 'height';
35
+ const aStart = a[axis];
36
+ const aEnd = aStart + a[size];
37
+ const bStart = b[axis];
38
+ const bEnd = bStart + b[size];
39
+ if (bStart >= aEnd)
40
+ return bStart - aEnd;
41
+ if (aStart >= bEnd)
42
+ return aStart - bEnd;
43
+ // Overlapping along this axis: negative distance, magnitude of the overlap.
44
+ return Math.max(aStart, bStart) - Math.min(aEnd, bEnd);
45
+ }
46
+ /** Rect area in px². */
47
+ export function area(r) {
48
+ return r.width * r.height;
49
+ }
50
+ /** Rect center point. */
51
+ export function center(r) {
52
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
53
+ }
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildLayoutTree = buildLayoutTree;
4
+ /**
5
+ * `ElementRecord.tag` is always `el.tagName.toLowerCase()` (see
6
+ * `snapshot/collect.ts`) — HTML and SVG tags are stored in the same case,
7
+ * so there is no "starts lowercase" signal left in the data to check by the
8
+ * time it reaches this module (the step-6 fix already drops the `<svg>`
9
+ * subtree, root included, at collection time). This set is the practical
10
+ * replacement: a fixed list of SVG-only tag names, kept as a defensive net
11
+ * in case `collectSnapshot`'s `SKIPPED_TAGS` ever changes to stop dropping
12
+ * the `<svg>` root itself. `svg` is deliberately absent — the root svg
13
+ * element is a leaf "icon" box per the classification pass below, not
14
+ * dropped.
15
+ */
16
+ const SVG_INTERNAL_TAGS = new Set([
17
+ 'g',
18
+ 'path',
19
+ 'rect',
20
+ 'circle',
21
+ 'ellipse',
22
+ 'line',
23
+ 'polyline',
24
+ 'polygon',
25
+ 'defs',
26
+ 'clippath',
27
+ 'lineargradient',
28
+ 'radialgradient',
29
+ 'stop',
30
+ 'use',
31
+ 'symbol',
32
+ 'mask',
33
+ 'pattern',
34
+ 'text',
35
+ 'tspan',
36
+ 'foreignobject',
37
+ ]);
38
+ const REPLACED_TAGS = new Set(['img', 'input', 'textarea', 'select', 'button']);
39
+ /** ~1px tolerance for the "rect equals parent's content box" collapse
40
+ * check — real layouts carry subpixel rounding. */
41
+ const COLLAPSE_TOLERANCE = 1;
42
+ function contentBox(el) {
43
+ return {
44
+ x: el.rect.x + el.padding.left + el.border.left,
45
+ y: el.rect.y + el.padding.top + el.border.top,
46
+ width: el.rect.width - el.padding.left - el.padding.right - el.border.left - el.border.right,
47
+ height: el.rect.height - el.padding.top - el.padding.bottom - el.border.top - el.border.bottom,
48
+ };
49
+ }
50
+ function rectsMatch(a, b, tolerance = COLLAPSE_TOLERANCE) {
51
+ return (Math.abs(a.x - b.x) <= tolerance &&
52
+ Math.abs(a.y - b.y) <= tolerance &&
53
+ Math.abs(a.width - b.width) <= tolerance &&
54
+ Math.abs(a.height - b.height) <= tolerance);
55
+ }
56
+ function hasVisibleBorder(el) {
57
+ return el.border.top > 0 || el.border.right > 0 || el.border.bottom > 0 || el.border.left > 0;
58
+ }
59
+ function sameBorder(a, b) {
60
+ return (a.border.top === b.border.top &&
61
+ a.border.right === b.border.right &&
62
+ a.border.bottom === b.border.bottom &&
63
+ a.border.left === b.border.left);
64
+ }
65
+ /** `rgba(r, g, b, 0)` / `transparent` are the only two forms
66
+ * `getComputedStyle` returns for "no background" — anything else paints
67
+ * something, even a translucent color. */
68
+ function hasVisibleBackground(el) {
69
+ const bg = el.backgroundColor.trim();
70
+ if (bg === '' || bg === 'transparent')
71
+ return false;
72
+ const alphaMatch = /rgba?\([^)]*,\s*([\d.]+)\s*\)/.exec(bg);
73
+ if (alphaMatch?.[1] !== undefined && Number.parseFloat(alphaMatch[1]) === 0)
74
+ return false;
75
+ return true;
76
+ }
77
+ /**
78
+ * Leaf if: has non-empty own text, is a replaced element or an SVG root, or
79
+ * carries a visible border/background distinct from its parent's (a
80
+ * decorated box like a card or a chip is treated as one atomic layout unit
81
+ * — its internal structure isn't relevant to gap/overlap/uniformity
82
+ * analysis at the level above it). `parent` is `null` only at a document
83
+ * root, where "distinct from parent" degrades to "has one at all".
84
+ */
85
+ function isLeaf(el, parent) {
86
+ if (el.text.trim().length > 0)
87
+ return true;
88
+ if (REPLACED_TAGS.has(el.tag))
89
+ return true;
90
+ if (el.tag === 'svg')
91
+ return true;
92
+ const ownBorder = hasVisibleBorder(el);
93
+ const ownBackground = hasVisibleBackground(el);
94
+ if (!parent)
95
+ return ownBorder || ownBackground;
96
+ if (ownBorder && !sameBorder(el, parent))
97
+ return true;
98
+ if (ownBackground && el.backgroundColor !== parent.backgroundColor)
99
+ return true;
100
+ return false;
101
+ }
102
+ /**
103
+ * Every flex container reported `row` until this took `flex-direction` into
104
+ * account, and `column` -- a value the grammar, the parser and the `Flow` type
105
+ * all carried -- was unreachable.
106
+ *
107
+ * It was not only a mislabelled token. The digest measures a child's gap along
108
+ * its parent's flow axis, so every `flex-direction: column` container had its
109
+ * children measured *horizontally*: a stack of full-width rows reported `→0`
110
+ * between each pair instead of the vertical gap that is actually there.
111
+ */
112
+ function flowFromDisplay(display, flexDirection) {
113
+ if (display.includes('grid'))
114
+ return 'grid';
115
+ if (display.includes('flex')) {
116
+ return flexDirection.startsWith('column') ? 'column' : 'row';
117
+ }
118
+ return 'none';
119
+ }
120
+ function buildLayoutTree(elements, options = {}) {
121
+ const byId = new Map(elements.map((el) => [el.id, el]));
122
+ const keepIds = options.keepIds;
123
+ let svgInternal = 0;
124
+ let invisible = 0;
125
+ let wrappersFolded = 0;
126
+ function keep(el) {
127
+ // `keepIds` has to be the first check, not the second: an SVG `<path>`
128
+ // (or any other tag in `SVG_INTERNAL_TAGS`) that `factsFor` found
129
+ // something to say about -- most commonly `clipped`, on an icon
130
+ // partially hidden by an ancestor's `overflow` -- used to be dropped
131
+ // here before this function ever looked at `keepIds`, because the
132
+ // SVG-internal check ran first and returned unconditionally. Checking
133
+ // `keepIds` first makes the override total, the same way the doc
134
+ // comment on `BuildLayoutTreeOptions.keepIds` already promises ("the
135
+ // digest passes every element carrying a fact") rather than true only
136
+ // for the one reason (`!el.visible`) that happened to be checked after
137
+ // it.
138
+ if (keepIds?.has(el.id))
139
+ return true;
140
+ if (SVG_INTERNAL_TAGS.has(el.tag)) {
141
+ svgInternal++;
142
+ return false;
143
+ }
144
+ if (!el.visible) {
145
+ invisible++;
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+ // Pass 1: filter + reparent. A dropped element's surviving children are
151
+ // spliced up to its nearest surviving ancestor's child list — dropping a
152
+ // node never deletes a subtree, only the one uninformative box.
153
+ function buildChildren(childIds) {
154
+ const result = [];
155
+ for (const id of childIds) {
156
+ const el = byId.get(id);
157
+ if (!el)
158
+ continue;
159
+ if (keep(el)) {
160
+ result.push({ el, children: buildChildren(el.childIds) });
161
+ }
162
+ else {
163
+ result.push(...buildChildren(el.childIds));
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+ // Pass 2 (collapse) + pass 3 (classify), combined per node bottom-up: a
169
+ // node's children must already be finalized before we can test whether
170
+ // this node has exactly one surviving child whose rect matches this
171
+ // node's own content box.
172
+ function build(raw, parent) {
173
+ let children = raw.children.map((child) => build(child, raw.el));
174
+ let wrapped = 0;
175
+ // The representative element for identity/classification purposes —
176
+ // starts as this node's own element, and becomes the innermost folded
177
+ // child's element as wrapper levels collapse into it. The box test
178
+ // itself always uses `raw.el`'s own content box (fixed for this node
179
+ // regardless of how many levels have folded in) — that's the box every
180
+ // absorbed child proved it matched.
181
+ let inner = raw.el;
182
+ const box = contentBox(raw.el);
183
+ while (children.length === 1) {
184
+ // A wrapper that itself carries a fact (`keepIds` -- the same set pass
185
+ // 1's `keep()` checks above, threaded in from `renderDigest` as every
186
+ // id `factsFor` found something to say about) must not have its
187
+ // identity silently swapped out for its child's here. Before this
188
+ // check, `inner`'s reassignment below was unconditional: the wrapper's
189
+ // own id -- and with it, the only thing that could carry a fact
190
+ // computed for the wrapper itself, most commonly `<body>`'s own
191
+ // `overflow-y` when `<body>` has exactly one child filling it -- simply
192
+ // stopped being reachable from any `LayoutNode` in the tree, with no
193
+ // line anywhere disclosing that it happened. `keepIds` already exists
194
+ // for exactly this guarantee ("the digest passes every element
195
+ // carrying a fact") but was only ever consulted by pass 1's filter;
196
+ // this is the same guarantee at the one other point in the pipeline
197
+ // that can erase an element's identity. Stopping the fold at this
198
+ // level -- rather than hoisting the fact onto the surviving child, or
199
+ // collapsing anyway with a disclosure -- keeps the fix inside this
200
+ // module and inside `keepIds`'s existing contract: the wrapper gets its
201
+ // own line, exactly the "direct" route every other fact-bearing
202
+ // element already uses, so no new disclosure format is needed and no
203
+ // fact ever needs re-attributing to an id that didn't measure it.
204
+ if (keepIds?.has(inner.id))
205
+ break;
206
+ const only = children[0];
207
+ if (only && rectsMatch(only.rect, box)) {
208
+ const onlyEl = byId.get(only.elementId);
209
+ if (!onlyEl)
210
+ break;
211
+ wrapped += 1 + only.wrapped;
212
+ wrappersFolded += 1;
213
+ inner = onlyEl;
214
+ children = only.children;
215
+ }
216
+ else {
217
+ break;
218
+ }
219
+ }
220
+ // `kind` is classification metadata only — it does not prune `children`.
221
+ // A card with a background distinct from its parent is a `leaf` in the
222
+ // sense that a digest/query consumer can treat it as one atomic visual
223
+ // unit, but `overlaps`/`siblingGaps`/`siblingUniformity` still need its
224
+ // real children (e.g. an absolutely-positioned badge nested inside a
225
+ // colored card) to be present in the tree to find them.
226
+ const leaf = isLeaf(inner, parent);
227
+ return {
228
+ elementId: inner.id,
229
+ tag: inner.tag,
230
+ kind: leaf ? 'leaf' : 'container',
231
+ flow: flowFromDisplay(inner.display, inner.flexDirection),
232
+ position: inner.position,
233
+ wrapped,
234
+ rect: inner.rect,
235
+ children,
236
+ };
237
+ }
238
+ const rootIds = elements.filter((el) => el.parentId === null).map((el) => el.id);
239
+ const rawRoots = buildChildren(rootIds);
240
+ const tree = rawRoots.map((raw) => build(raw, null));
241
+ return {
242
+ tree,
243
+ dropped: { svgInternal, invisible, wrappers: wrappersFolded },
244
+ };
245
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builds a `LayoutNode` tree from a flat `ElementRecord[]` snapshot — the
3
+ * keystone module of `src/analysis/`: `neighbors.ts` and `uniformity.ts`
4
+ * both walk the tree this produces, and the future digest renderer renders
5
+ * it directly. Get this right; everything else depends on it.
6
+ *
7
+ * Three passes, each pure and independently reasoned about:
8
+ * 1. Filter + reparent — drop elements that aren't layout-meaningful
9
+ * (SVG-internals, invisible, zero-size), splicing any surviving
10
+ * children up to the nearest surviving ancestor so a dropped node never
11
+ * silently deletes a whole subtree.
12
+ * 2. Collapse — fold a lone wrapper child into its parent when the child's
13
+ * rect matches the parent's content box, tracked as `wrapped`, unless the
14
+ * wrapper itself is in `keepIds` (it carries a fact of its own), in which
15
+ * case it keeps its own line instead of being folded away.
16
+ * 3. Classify — decide `leaf` vs `container` per surviving node.
17
+ */
18
+ import type { ElementRecord } from "../snapshot/types.cjs";
19
+ import type { LayoutTreeResult } from "./types.cjs";
20
+ export interface BuildLayoutTreeOptions {
21
+ /**
22
+ * Elements that must survive whatever their geometry says -- checked at
23
+ * both points in this file that can otherwise erase an element's identity:
24
+ * pass 1's filter (`keep()`, below) and pass 2's wrapper collapse (the
25
+ * `while (children.length === 1)` loop in `build()`).
26
+ *
27
+ * Pass 1 drops anything with no visible box, which is right for the
28
+ * hundreds of zero-size nodes on a real page and catastrophically wrong for
29
+ * the one that matters: a column that collapsed to `0x1239` *around a page of
30
+ * content* has no visible box either. Dropping it deletes the element the
31
+ * whole finding is about, which is the prototype's failure mode wearing a
32
+ * different hat -- the measurement exists and the rendering throws it away.
33
+ *
34
+ * Pass 2's wrapper collapse can do the same thing a different way: it
35
+ * doesn't drop the wrapper's `ElementRecord`, but it does replace the
36
+ * wrapper's identity with its single child's in the rendered tree, and a
37
+ * fact keyed to the wrapper's own id (most commonly `<body>`'s own
38
+ * `overflow-y`) has nowhere left to attach once no `LayoutNode` carries
39
+ * that id anymore.
40
+ *
41
+ * The digest passes every element carrying a fact.
42
+ */
43
+ keepIds?: ReadonlySet<string>;
44
+ }
45
+ export declare function buildLayoutTree(elements: ElementRecord[], options?: BuildLayoutTreeOptions): LayoutTreeResult;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builds a `LayoutNode` tree from a flat `ElementRecord[]` snapshot — the
3
+ * keystone module of `src/analysis/`: `neighbors.ts` and `uniformity.ts`
4
+ * both walk the tree this produces, and the future digest renderer renders
5
+ * it directly. Get this right; everything else depends on it.
6
+ *
7
+ * Three passes, each pure and independently reasoned about:
8
+ * 1. Filter + reparent — drop elements that aren't layout-meaningful
9
+ * (SVG-internals, invisible, zero-size), splicing any surviving
10
+ * children up to the nearest surviving ancestor so a dropped node never
11
+ * silently deletes a whole subtree.
12
+ * 2. Collapse — fold a lone wrapper child into its parent when the child's
13
+ * rect matches the parent's content box, tracked as `wrapped`, unless the
14
+ * wrapper itself is in `keepIds` (it carries a fact of its own), in which
15
+ * case it keeps its own line instead of being folded away.
16
+ * 3. Classify — decide `leaf` vs `container` per surviving node.
17
+ */
18
+ import type { ElementRecord } from "../snapshot/types.js";
19
+ import type { LayoutTreeResult } from "./types.js";
20
+ export interface BuildLayoutTreeOptions {
21
+ /**
22
+ * Elements that must survive whatever their geometry says -- checked at
23
+ * both points in this file that can otherwise erase an element's identity:
24
+ * pass 1's filter (`keep()`, below) and pass 2's wrapper collapse (the
25
+ * `while (children.length === 1)` loop in `build()`).
26
+ *
27
+ * Pass 1 drops anything with no visible box, which is right for the
28
+ * hundreds of zero-size nodes on a real page and catastrophically wrong for
29
+ * the one that matters: a column that collapsed to `0x1239` *around a page of
30
+ * content* has no visible box either. Dropping it deletes the element the
31
+ * whole finding is about, which is the prototype's failure mode wearing a
32
+ * different hat -- the measurement exists and the rendering throws it away.
33
+ *
34
+ * Pass 2's wrapper collapse can do the same thing a different way: it
35
+ * doesn't drop the wrapper's `ElementRecord`, but it does replace the
36
+ * wrapper's identity with its single child's in the rendered tree, and a
37
+ * fact keyed to the wrapper's own id (most commonly `<body>`'s own
38
+ * `overflow-y`) has nowhere left to attach once no `LayoutNode` carries
39
+ * that id anymore.
40
+ *
41
+ * The digest passes every element carrying a fact.
42
+ */
43
+ keepIds?: ReadonlySet<string>;
44
+ }
45
+ export declare function buildLayoutTree(elements: ElementRecord[], options?: BuildLayoutTreeOptions): LayoutTreeResult;