@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,316 @@
1
+ /**
2
+ * Color, composited and disclosed -- never adjudicated.
3
+ *
4
+ * The design of record is `docs/notes/03-color.md`; this file is stage 1 of
5
+ * it. Three things carry over from that note verbatim, because they are the
6
+ * whole shape of the module:
7
+ *
8
+ * 1. **The backdrop composite runs Node-side, over `ElementRecord[]`, exactly
9
+ * like `clip.ts`.** Both capture paths already produce the raw fields this
10
+ * needs (`backgroundColor`, `backgroundImage`, `opacity`, the overlap
11
+ * inputs); nothing here touches a page or a browser, so both paths agree
12
+ * on every derived color fact by construction.
13
+ * 2. **`ratio` is a number or it is `unmeasured (<reason>)`. Never a verdict.**
14
+ * No output token here ever says `pass`, `fail`, `AA`, `AAA`, `low`, or
15
+ * `poor`. §5 of the design note draws this line in four rules; this module
16
+ * is the code that holds it.
17
+ * 3. **A gradient, an image, or a non-ancestor overlay makes "the background
18
+ * color" not exist**, and printing a number anyway is fabrication, not
19
+ * suppression-avoidance (design note §5's rule 4). `blendedBackgroundColors`
20
+ * -- CDP's own composite -- gets this wrong on exactly these two shapes
21
+ * (verified in the design note's probes, and again in
22
+ * `test/browser/color-backdrop.test.ts`), which is why it is a test oracle
23
+ * for the alpha-chain case it gets right and never a source at runtime.
24
+ *
25
+ * The color-space conversions (`oklch`/`oklab`/`lab`/`lch`/`color(srgb*
26
+ * )`/`color(display-p3 ...)`) are verified against real Chromium canvas
27
+ * readback, not derived from memory of the spec math alone --
28
+ * `test/browser/color-space-conversion.test.ts` paints each one and diffs
29
+ * channels, per the design note's own §10 experiment 4.
30
+ */
31
+ import type { DocumentBackground, ElementRecord } from "./types.cjs";
32
+ /** 0-255 per channel, alpha 0-1. */
33
+ export interface RGBA {
34
+ r: number;
35
+ g: number;
36
+ b: number;
37
+ a: number;
38
+ }
39
+ export type ParsedColor = {
40
+ kind: 'resolved';
41
+ rgba: RGBA;
42
+ } | {
43
+ kind: 'verbatim';
44
+ raw: string;
45
+ };
46
+ /**
47
+ * Parse a computed-style color string to sRGB, or refuse honestly.
48
+ *
49
+ * Every space `docs/notes/03-color.md` §3 calls out as a real capture
50
+ * problem -- `oklch`, `oklab`, `lab`, `lch`, `color(srgb ...)`,
51
+ * `color(srgb-linear ...)`, `color(display-p3 ...)` -- converts. Anything
52
+ * else (a future space, a syntax this parser does not recognise) returns the
53
+ * raw string verbatim rather than guessing: rule 4 of the design note's §5
54
+ * applies to color parsing exactly as it does to the ratio itself.
55
+ */
56
+ export declare function parseCssColor(raw: string): ParsedColor;
57
+ /** `#rrggbb`, or `#rrggbbaa` when `alpha` is given and < 1. */
58
+ export declare function toHex(rgb: {
59
+ r: number;
60
+ g: number;
61
+ b: number;
62
+ }, alpha?: number): string;
63
+ /** WCAG 2.x relative luminance. The name of a formula, never a threshold. */
64
+ export declare function relativeLuminance(rgb: {
65
+ r: number;
66
+ g: number;
67
+ b: number;
68
+ }): number;
69
+ /** WCAG 2.x contrast ratio between two opaque sRGB colors. */
70
+ export declare function contrastRatio(a: {
71
+ r: number;
72
+ g: number;
73
+ b: number;
74
+ }, b: {
75
+ r: number;
76
+ g: number;
77
+ b: number;
78
+ }): number;
79
+ export type Backdrop = {
80
+ kind: 'opaque';
81
+ color: string;
82
+ via: string;
83
+ self: boolean;
84
+ layers: number;
85
+ } | {
86
+ kind: 'gradient';
87
+ from: string;
88
+ to: string;
89
+ via: string;
90
+ } | {
91
+ kind: 'image';
92
+ via: string;
93
+ coverage: ImageCoverage;
94
+ } | {
95
+ kind: 'overlay';
96
+ reason: 'under' | 'behind';
97
+ via: string;
98
+ }
99
+ /**
100
+ * `via`'s own `backgroundImage` is `null` -- this specific capture never
101
+ * measured it (A3; see `ElementRecord.backgroundImage`'s doc comment and
102
+ * `bundle.ts#normalizeElement`). Compositing cannot honestly continue past
103
+ * an element whose background-image is unknown: it might be a gradient or
104
+ * a photo sitting behind the text, and there is no way to tell from here.
105
+ * Refuse to guess, the same discipline `detectOverlay` already applies to
106
+ * a missing `hitStackIds`/`paintOrder`.
107
+ */
108
+ | {
109
+ kind: 'not-captured';
110
+ via: string;
111
+ }
112
+ /**
113
+ * The ancestor walk ran off the top of `<body>` with nothing left to
114
+ * check, and either no `DocumentBackground` was supplied at all (a caller
115
+ * that has not been updated to pass one -- see this module's header) or
116
+ * the one supplied cannot honestly resolve to a single color (an
117
+ * unconvertible `background-color`, or a `color-scheme` that lists both
118
+ * `light` and `dark` with no way to know which the reader's browser would
119
+ * pick). See `canvasBackdrop` and A2.
120
+ */
121
+ | {
122
+ kind: 'canvas';
123
+ };
124
+ /** `<html>`'s own background-color/color-scheme -- see `DocumentBackground`
125
+ * in `snapshot/types.ts`, and this module's header for why a compositing
126
+ * walk needs it. Re-exported under this name here because `resolveBackdrop`
127
+ * et al. take it as a plain, optional parameter (never a required one: every
128
+ * existing caller that does not yet pass one -- see A2's routing note --
129
+ * must keep compiling and must keep refusing to guess, never regress to
130
+ * assuming white). */
131
+ export type CanvasBackground = DocumentBackground;
132
+ /**
133
+ * The `image` backdrop's weakest signal, strengthened: a real-page soak
134
+ * found `unmeasured (image <id>)` printing identically for a 12x12 decorative
135
+ * sort-arrow in one corner of a table header and a 1440x600 full-bleed hero
136
+ * photo -- an agent cannot tell "irrelevant corner decoration" from
137
+ * "genuinely unmeasurable backdrop" from the token alone, and would spend a
138
+ * screenshot round-trip finding out something already on the record.
139
+ *
140
+ * `box` is the rendered size of the element that actually carries the
141
+ * `background-image` (already captured, no new measurement); `of` is the
142
+ * text element's own box, so `image e_th (12x12 of 180x40)` and
143
+ * `image e_hero (1440x600 of 1440x600)` read as the different sentences they
144
+ * are. This is a size proxy, not the image's true painted footprint --
145
+ * `background-size`/`background-position` can still make a small box show a
146
+ * cropped, dominant image, or a large box show a small tiled icon -- so it
147
+ * narrows the question rather than closing it, honestly.
148
+ */
149
+ export interface ImageCoverage {
150
+ boxWidth: number;
151
+ boxHeight: number;
152
+ ofWidth: number;
153
+ ofHeight: number;
154
+ }
155
+ /** One layer the ancestor walk actually visited: its own color and alpha, and
156
+ * the element it came from. Diagnostic detail for `q color <id>`'s long form
157
+ * ("the full composite chain") -- not needed by the grammar line itself,
158
+ * which only ever shows the final result plus a layer *count*. */
159
+ export interface BackdropLayer {
160
+ id: string;
161
+ color: string;
162
+ alpha: number;
163
+ }
164
+ export interface ResolvedBackdrop {
165
+ backdrop: Backdrop;
166
+ /** The final composited, opaque sRGB color -- `null` whenever `backdrop`
167
+ * is not a single resolved color (gradient, image, overlay, not-captured,
168
+ * canvas) or its terminating layer's own color could not be converted. */
169
+ rgb: {
170
+ r: number;
171
+ g: number;
172
+ b: number;
173
+ } | null;
174
+ /** Every layer visited, from `el` itself outward, including the
175
+ * terminating one (opaque, canvas, or the unresolvable/gradient/image/
176
+ * not-captured layer that ended the walk). */
177
+ chain: BackdropLayer[];
178
+ }
179
+ /**
180
+ * Walk up from `el` accumulating translucent `background-color` layers
181
+ * *and* CSS `opacity` until something fully opaque is reached, then report
182
+ * the composited result -- an incremental left-to-right fold rather than
183
+ * "collect layers, then composite once at the end", specifically so
184
+ * `opacity` folds in correctly (A5): unlike a plain `background-color`
185
+ * alpha, `opacity` scales *everything already composited so far* as one
186
+ * group at the exact point it is applied, so each ancestor's own
187
+ * contribution has to be resolved against what is beneath it before the
188
+ * next ancestor's `opacity` scales the combined result again. The pure
189
+ * `background-color`-only case (no `opacity < 1` anywhere in the chain)
190
+ * reduces to exactly the old flat-stack composite -- *verified* exact
191
+ * against Chromium's own `blendedBackgroundColors` on an alpha chain
192
+ * (`test/browser/color-backdrop.test.ts`) -- and the opacity-aware general
193
+ * case is verified against real Chromium canvas pixel readback for both a
194
+ * single opacity boundary and nested multi-level opacity (see this
195
+ * module's header and `test/browser/color-backdrop.test.ts`'s A5 cases).
196
+ *
197
+ * Aborts to a `gradient`/`image`/`not-captured` backdrop the instant any
198
+ * layer's own `backgroundImage` says so (A3), and to `canvas` when the walk
199
+ * runs off the top of `<body>` with no way to know the real default fill
200
+ * (A2, see `canvasBackdrop`). A non-ancestor overlay is checked by the
201
+ * caller before this ever runs (see `colorFactFor`), since an overlay makes
202
+ * the ancestor chain irrelevant regardless of what it would have found.
203
+ */
204
+ export declare function resolveBackdrop(el: ElementRecord, byId: Map<string, ElementRecord>, canvas?: CanvasBackground | null): ResolvedBackdrop;
205
+ /**
206
+ * A non-ancestor element painted `under` (in front of, obscuring the text
207
+ * itself) or `behind` (between the text and where the ancestor walk would
208
+ * have looked) this one -- the two `incompleteData` cases
209
+ * `blendedBackgroundColors` gets silently wrong (design note §3): a
210
+ * `rgba(255,0,0,0.8)` sibling scrim painted directly over text reads back as
211
+ * `rgb(255,255,255)`, the page background, as if the overlay were not there.
212
+ *
213
+ * Two independent input signals, one per capture path (design note §7):
214
+ * `el.hitStackIds` (the `walk` path's `elementsFromPoint` result, already
215
+ * ordered front-to-back) when present, else `paintOrder` plus rect
216
+ * intersection (the `cdp` path, Node-side, whole-rect rather than
217
+ * centre-point). Neither present -- a hand-built fixture, a bundle written
218
+ * before this capability existed -- reports no overlay rather than guessing:
219
+ * rule 4 of the design note's §5 (refuse rather than fabricate) applies here
220
+ * exactly as it does to the ratio itself.
221
+ */
222
+ export declare function detectOverlay(el: ElementRecord, byId: Map<string, ElementRecord>, elements: ElementRecord[]): {
223
+ reason: 'under' | 'behind';
224
+ via: string;
225
+ } | null;
226
+ export type RatioReason = 'gradient' | 'image' | 'under' | 'behind' | 'color-space' | 'not-captured' | 'canvas';
227
+ export interface ColorFact {
228
+ elementId: string;
229
+ /** The effective ink: computed `color`, composited at effective opacity,
230
+ * as lowercase hex (with an alpha suffix below full opacity) -- or the raw
231
+ * CSS string verbatim when the color space could not be converted. */
232
+ fg: string;
233
+ backdrop: Backdrop;
234
+ /** WCAG 2.x contrast ratio, or `null` when it cannot be honestly computed
235
+ * -- see `ratioReason`. Never a verdict. */
236
+ ratio: number | null;
237
+ ratioReason: RatioReason | null;
238
+ fontSizePx: number;
239
+ fontWeight: number;
240
+ /** The CSS `opacity` multiplier, present only when it is below 1 -- "so the
241
+ * reader can recover the raw value" (design note §4). */
242
+ effectiveOpacity: number | null;
243
+ shadowColor: string | null;
244
+ strokeWidthPx: number | null;
245
+ strokeColor: string | null;
246
+ }
247
+ /**
248
+ * The color fact for one element, or `null` when the measurement gate does
249
+ * not apply: "every visible element with its own text" (design note §4) --
250
+ * no text, no ink-on-backdrop question to answer.
251
+ *
252
+ * A6 note: a closed `<sl-dialog>` (`opacity: 0` on an ancestor) still gets a
253
+ * fact here for each visible-by-its-own-styling descendant -- `el.visible`
254
+ * only reflects the element's *own* opacity/display/visibility/size, not an
255
+ * ancestor's. That fact's `ratio` genuinely does come out `1.00` (ink
256
+ * composited to exactly equal its own backdrop at zero *effective* opacity)
257
+ * -- mathematically correct, not fabricated, and never suppressed here:
258
+ * "facts, with counter-facts, never adjudicate" means this module does not
259
+ * get to decide the fact is uninteresting and drop it. `effectiveOpacity`
260
+ * below is the counter-fact that explains it (`opacity ×0`, kept on
261
+ * `ColorFact` and, via `ColorPair.effectiveOpacity`, on `q color`'s grouped
262
+ * rows too -- see that field's doc comment) so a reader sees *why* the
263
+ * ratio is round, rather than the fact disappearing and the explanation
264
+ * disappearing with it.
265
+ */
266
+ export declare function colorFactFor(el: ElementRecord, byId: Map<string, ElementRecord>, elements: ElementRecord[], canvas?: CanvasBackground | null): ColorFact | null;
267
+ /** Every color fact the capture supports, in document order -- the gate from
268
+ * `colorFactFor` applied once per element. */
269
+ export declare function colorFactsForAll(elements: ElementRecord[], canvas?: CanvasBackground | null): ColorFact[];
270
+ /** Just the `<bg>` half of the grammar -- reused by `q color`'s pair table
271
+ * and the digest header's `lowest` clause, which both need it without the
272
+ * rest of the line. */
273
+ export declare function backdropLabel(backdrop: Backdrop): string;
274
+ /**
275
+ * `color <fg> on <bg>; ratio <r>; <size>px/<weight>[; opacity ×<o>][; shadow
276
+ * <color>][; stroke <w>px <color>]` -- `docs/notes/03-color.md` §4, exactly.
277
+ */
278
+ export declare function renderColorFact(fact: ColorFact): string;
279
+ export interface ColorPair {
280
+ fg: string;
281
+ bgLabel: string;
282
+ ratio: number | null;
283
+ ratioReason: RatioReason | null;
284
+ elementIds: string[];
285
+ fontSizesPx: number[];
286
+ weights: number[];
287
+ /**
288
+ * The CSS `opacity` multiplier, present only when it is below 1 -- same
289
+ * meaning and same "present only below 1" convention as
290
+ * `ColorFact.effectiveOpacity`, taken from the first fact in the group
291
+ * exactly like `ratio`/`ratioReason` already are (elements sharing one
292
+ * (ink, backdrop) pair by construction share the composited result, and
293
+ * in the overwhelming common case share the same ancestor chain too).
294
+ *
295
+ * Added for A6: `q color`'s pair table used to have no way to carry this
296
+ * at all, so even a caller that *wanted* to show `opacity ×0` (or `×0.5`)
297
+ * next to a row -- the fact that explains a suspiciously round ratio like
298
+ * `1.00` on an ancestor-hidden element -- had nothing to read it from off
299
+ * a `ColorPair` directly. `query.ts`'s own fix for the same gap
300
+ * (`pairOpacities`) instead recovers the full, deduped set of a pair's
301
+ * member opacities from the ungrouped facts by `elementIds`, which this
302
+ * single first-fact value cannot replace when a pair's members disagree
303
+ * (rare, but possible) -- both are kept: this one for a caller that only
304
+ * needs "does this pair carry a reduced-opacity member" cheaply, without
305
+ * re-deriving a `factsById` map of its own.
306
+ */
307
+ effectiveOpacity: number | null;
308
+ }
309
+ /**
310
+ * One row per (ink, backdrop) pair, not per element -- "thousands of
311
+ * elements compress to dozens of rows" (design note §6). `fg` and the
312
+ * rendered `<bg>` form together are the grouping key: two elements with the
313
+ * same ink over the same backdrop are, by construction, the same measured
314
+ * ratio.
315
+ */
316
+ export declare function groupColorFacts(facts: ColorFact[]): ColorPair[];
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Color, composited and disclosed -- never adjudicated.
3
+ *
4
+ * The design of record is `docs/notes/03-color.md`; this file is stage 1 of
5
+ * it. Three things carry over from that note verbatim, because they are the
6
+ * whole shape of the module:
7
+ *
8
+ * 1. **The backdrop composite runs Node-side, over `ElementRecord[]`, exactly
9
+ * like `clip.ts`.** Both capture paths already produce the raw fields this
10
+ * needs (`backgroundColor`, `backgroundImage`, `opacity`, the overlap
11
+ * inputs); nothing here touches a page or a browser, so both paths agree
12
+ * on every derived color fact by construction.
13
+ * 2. **`ratio` is a number or it is `unmeasured (<reason>)`. Never a verdict.**
14
+ * No output token here ever says `pass`, `fail`, `AA`, `AAA`, `low`, or
15
+ * `poor`. §5 of the design note draws this line in four rules; this module
16
+ * is the code that holds it.
17
+ * 3. **A gradient, an image, or a non-ancestor overlay makes "the background
18
+ * color" not exist**, and printing a number anyway is fabrication, not
19
+ * suppression-avoidance (design note §5's rule 4). `blendedBackgroundColors`
20
+ * -- CDP's own composite -- gets this wrong on exactly these two shapes
21
+ * (verified in the design note's probes, and again in
22
+ * `test/browser/color-backdrop.test.ts`), which is why it is a test oracle
23
+ * for the alpha-chain case it gets right and never a source at runtime.
24
+ *
25
+ * The color-space conversions (`oklch`/`oklab`/`lab`/`lch`/`color(srgb*
26
+ * )`/`color(display-p3 ...)`) are verified against real Chromium canvas
27
+ * readback, not derived from memory of the spec math alone --
28
+ * `test/browser/color-space-conversion.test.ts` paints each one and diffs
29
+ * channels, per the design note's own §10 experiment 4.
30
+ */
31
+ import type { DocumentBackground, ElementRecord } from "./types.js";
32
+ /** 0-255 per channel, alpha 0-1. */
33
+ export interface RGBA {
34
+ r: number;
35
+ g: number;
36
+ b: number;
37
+ a: number;
38
+ }
39
+ export type ParsedColor = {
40
+ kind: 'resolved';
41
+ rgba: RGBA;
42
+ } | {
43
+ kind: 'verbatim';
44
+ raw: string;
45
+ };
46
+ /**
47
+ * Parse a computed-style color string to sRGB, or refuse honestly.
48
+ *
49
+ * Every space `docs/notes/03-color.md` §3 calls out as a real capture
50
+ * problem -- `oklch`, `oklab`, `lab`, `lch`, `color(srgb ...)`,
51
+ * `color(srgb-linear ...)`, `color(display-p3 ...)` -- converts. Anything
52
+ * else (a future space, a syntax this parser does not recognise) returns the
53
+ * raw string verbatim rather than guessing: rule 4 of the design note's §5
54
+ * applies to color parsing exactly as it does to the ratio itself.
55
+ */
56
+ export declare function parseCssColor(raw: string): ParsedColor;
57
+ /** `#rrggbb`, or `#rrggbbaa` when `alpha` is given and < 1. */
58
+ export declare function toHex(rgb: {
59
+ r: number;
60
+ g: number;
61
+ b: number;
62
+ }, alpha?: number): string;
63
+ /** WCAG 2.x relative luminance. The name of a formula, never a threshold. */
64
+ export declare function relativeLuminance(rgb: {
65
+ r: number;
66
+ g: number;
67
+ b: number;
68
+ }): number;
69
+ /** WCAG 2.x contrast ratio between two opaque sRGB colors. */
70
+ export declare function contrastRatio(a: {
71
+ r: number;
72
+ g: number;
73
+ b: number;
74
+ }, b: {
75
+ r: number;
76
+ g: number;
77
+ b: number;
78
+ }): number;
79
+ export type Backdrop = {
80
+ kind: 'opaque';
81
+ color: string;
82
+ via: string;
83
+ self: boolean;
84
+ layers: number;
85
+ } | {
86
+ kind: 'gradient';
87
+ from: string;
88
+ to: string;
89
+ via: string;
90
+ } | {
91
+ kind: 'image';
92
+ via: string;
93
+ coverage: ImageCoverage;
94
+ } | {
95
+ kind: 'overlay';
96
+ reason: 'under' | 'behind';
97
+ via: string;
98
+ }
99
+ /**
100
+ * `via`'s own `backgroundImage` is `null` -- this specific capture never
101
+ * measured it (A3; see `ElementRecord.backgroundImage`'s doc comment and
102
+ * `bundle.ts#normalizeElement`). Compositing cannot honestly continue past
103
+ * an element whose background-image is unknown: it might be a gradient or
104
+ * a photo sitting behind the text, and there is no way to tell from here.
105
+ * Refuse to guess, the same discipline `detectOverlay` already applies to
106
+ * a missing `hitStackIds`/`paintOrder`.
107
+ */
108
+ | {
109
+ kind: 'not-captured';
110
+ via: string;
111
+ }
112
+ /**
113
+ * The ancestor walk ran off the top of `<body>` with nothing left to
114
+ * check, and either no `DocumentBackground` was supplied at all (a caller
115
+ * that has not been updated to pass one -- see this module's header) or
116
+ * the one supplied cannot honestly resolve to a single color (an
117
+ * unconvertible `background-color`, or a `color-scheme` that lists both
118
+ * `light` and `dark` with no way to know which the reader's browser would
119
+ * pick). See `canvasBackdrop` and A2.
120
+ */
121
+ | {
122
+ kind: 'canvas';
123
+ };
124
+ /** `<html>`'s own background-color/color-scheme -- see `DocumentBackground`
125
+ * in `snapshot/types.ts`, and this module's header for why a compositing
126
+ * walk needs it. Re-exported under this name here because `resolveBackdrop`
127
+ * et al. take it as a plain, optional parameter (never a required one: every
128
+ * existing caller that does not yet pass one -- see A2's routing note --
129
+ * must keep compiling and must keep refusing to guess, never regress to
130
+ * assuming white). */
131
+ export type CanvasBackground = DocumentBackground;
132
+ /**
133
+ * The `image` backdrop's weakest signal, strengthened: a real-page soak
134
+ * found `unmeasured (image <id>)` printing identically for a 12x12 decorative
135
+ * sort-arrow in one corner of a table header and a 1440x600 full-bleed hero
136
+ * photo -- an agent cannot tell "irrelevant corner decoration" from
137
+ * "genuinely unmeasurable backdrop" from the token alone, and would spend a
138
+ * screenshot round-trip finding out something already on the record.
139
+ *
140
+ * `box` is the rendered size of the element that actually carries the
141
+ * `background-image` (already captured, no new measurement); `of` is the
142
+ * text element's own box, so `image e_th (12x12 of 180x40)` and
143
+ * `image e_hero (1440x600 of 1440x600)` read as the different sentences they
144
+ * are. This is a size proxy, not the image's true painted footprint --
145
+ * `background-size`/`background-position` can still make a small box show a
146
+ * cropped, dominant image, or a large box show a small tiled icon -- so it
147
+ * narrows the question rather than closing it, honestly.
148
+ */
149
+ export interface ImageCoverage {
150
+ boxWidth: number;
151
+ boxHeight: number;
152
+ ofWidth: number;
153
+ ofHeight: number;
154
+ }
155
+ /** One layer the ancestor walk actually visited: its own color and alpha, and
156
+ * the element it came from. Diagnostic detail for `q color <id>`'s long form
157
+ * ("the full composite chain") -- not needed by the grammar line itself,
158
+ * which only ever shows the final result plus a layer *count*. */
159
+ export interface BackdropLayer {
160
+ id: string;
161
+ color: string;
162
+ alpha: number;
163
+ }
164
+ export interface ResolvedBackdrop {
165
+ backdrop: Backdrop;
166
+ /** The final composited, opaque sRGB color -- `null` whenever `backdrop`
167
+ * is not a single resolved color (gradient, image, overlay, not-captured,
168
+ * canvas) or its terminating layer's own color could not be converted. */
169
+ rgb: {
170
+ r: number;
171
+ g: number;
172
+ b: number;
173
+ } | null;
174
+ /** Every layer visited, from `el` itself outward, including the
175
+ * terminating one (opaque, canvas, or the unresolvable/gradient/image/
176
+ * not-captured layer that ended the walk). */
177
+ chain: BackdropLayer[];
178
+ }
179
+ /**
180
+ * Walk up from `el` accumulating translucent `background-color` layers
181
+ * *and* CSS `opacity` until something fully opaque is reached, then report
182
+ * the composited result -- an incremental left-to-right fold rather than
183
+ * "collect layers, then composite once at the end", specifically so
184
+ * `opacity` folds in correctly (A5): unlike a plain `background-color`
185
+ * alpha, `opacity` scales *everything already composited so far* as one
186
+ * group at the exact point it is applied, so each ancestor's own
187
+ * contribution has to be resolved against what is beneath it before the
188
+ * next ancestor's `opacity` scales the combined result again. The pure
189
+ * `background-color`-only case (no `opacity < 1` anywhere in the chain)
190
+ * reduces to exactly the old flat-stack composite -- *verified* exact
191
+ * against Chromium's own `blendedBackgroundColors` on an alpha chain
192
+ * (`test/browser/color-backdrop.test.ts`) -- and the opacity-aware general
193
+ * case is verified against real Chromium canvas pixel readback for both a
194
+ * single opacity boundary and nested multi-level opacity (see this
195
+ * module's header and `test/browser/color-backdrop.test.ts`'s A5 cases).
196
+ *
197
+ * Aborts to a `gradient`/`image`/`not-captured` backdrop the instant any
198
+ * layer's own `backgroundImage` says so (A3), and to `canvas` when the walk
199
+ * runs off the top of `<body>` with no way to know the real default fill
200
+ * (A2, see `canvasBackdrop`). A non-ancestor overlay is checked by the
201
+ * caller before this ever runs (see `colorFactFor`), since an overlay makes
202
+ * the ancestor chain irrelevant regardless of what it would have found.
203
+ */
204
+ export declare function resolveBackdrop(el: ElementRecord, byId: Map<string, ElementRecord>, canvas?: CanvasBackground | null): ResolvedBackdrop;
205
+ /**
206
+ * A non-ancestor element painted `under` (in front of, obscuring the text
207
+ * itself) or `behind` (between the text and where the ancestor walk would
208
+ * have looked) this one -- the two `incompleteData` cases
209
+ * `blendedBackgroundColors` gets silently wrong (design note §3): a
210
+ * `rgba(255,0,0,0.8)` sibling scrim painted directly over text reads back as
211
+ * `rgb(255,255,255)`, the page background, as if the overlay were not there.
212
+ *
213
+ * Two independent input signals, one per capture path (design note §7):
214
+ * `el.hitStackIds` (the `walk` path's `elementsFromPoint` result, already
215
+ * ordered front-to-back) when present, else `paintOrder` plus rect
216
+ * intersection (the `cdp` path, Node-side, whole-rect rather than
217
+ * centre-point). Neither present -- a hand-built fixture, a bundle written
218
+ * before this capability existed -- reports no overlay rather than guessing:
219
+ * rule 4 of the design note's §5 (refuse rather than fabricate) applies here
220
+ * exactly as it does to the ratio itself.
221
+ */
222
+ export declare function detectOverlay(el: ElementRecord, byId: Map<string, ElementRecord>, elements: ElementRecord[]): {
223
+ reason: 'under' | 'behind';
224
+ via: string;
225
+ } | null;
226
+ export type RatioReason = 'gradient' | 'image' | 'under' | 'behind' | 'color-space' | 'not-captured' | 'canvas';
227
+ export interface ColorFact {
228
+ elementId: string;
229
+ /** The effective ink: computed `color`, composited at effective opacity,
230
+ * as lowercase hex (with an alpha suffix below full opacity) -- or the raw
231
+ * CSS string verbatim when the color space could not be converted. */
232
+ fg: string;
233
+ backdrop: Backdrop;
234
+ /** WCAG 2.x contrast ratio, or `null` when it cannot be honestly computed
235
+ * -- see `ratioReason`. Never a verdict. */
236
+ ratio: number | null;
237
+ ratioReason: RatioReason | null;
238
+ fontSizePx: number;
239
+ fontWeight: number;
240
+ /** The CSS `opacity` multiplier, present only when it is below 1 -- "so the
241
+ * reader can recover the raw value" (design note §4). */
242
+ effectiveOpacity: number | null;
243
+ shadowColor: string | null;
244
+ strokeWidthPx: number | null;
245
+ strokeColor: string | null;
246
+ }
247
+ /**
248
+ * The color fact for one element, or `null` when the measurement gate does
249
+ * not apply: "every visible element with its own text" (design note §4) --
250
+ * no text, no ink-on-backdrop question to answer.
251
+ *
252
+ * A6 note: a closed `<sl-dialog>` (`opacity: 0` on an ancestor) still gets a
253
+ * fact here for each visible-by-its-own-styling descendant -- `el.visible`
254
+ * only reflects the element's *own* opacity/display/visibility/size, not an
255
+ * ancestor's. That fact's `ratio` genuinely does come out `1.00` (ink
256
+ * composited to exactly equal its own backdrop at zero *effective* opacity)
257
+ * -- mathematically correct, not fabricated, and never suppressed here:
258
+ * "facts, with counter-facts, never adjudicate" means this module does not
259
+ * get to decide the fact is uninteresting and drop it. `effectiveOpacity`
260
+ * below is the counter-fact that explains it (`opacity ×0`, kept on
261
+ * `ColorFact` and, via `ColorPair.effectiveOpacity`, on `q color`'s grouped
262
+ * rows too -- see that field's doc comment) so a reader sees *why* the
263
+ * ratio is round, rather than the fact disappearing and the explanation
264
+ * disappearing with it.
265
+ */
266
+ export declare function colorFactFor(el: ElementRecord, byId: Map<string, ElementRecord>, elements: ElementRecord[], canvas?: CanvasBackground | null): ColorFact | null;
267
+ /** Every color fact the capture supports, in document order -- the gate from
268
+ * `colorFactFor` applied once per element. */
269
+ export declare function colorFactsForAll(elements: ElementRecord[], canvas?: CanvasBackground | null): ColorFact[];
270
+ /** Just the `<bg>` half of the grammar -- reused by `q color`'s pair table
271
+ * and the digest header's `lowest` clause, which both need it without the
272
+ * rest of the line. */
273
+ export declare function backdropLabel(backdrop: Backdrop): string;
274
+ /**
275
+ * `color <fg> on <bg>; ratio <r>; <size>px/<weight>[; opacity ×<o>][; shadow
276
+ * <color>][; stroke <w>px <color>]` -- `docs/notes/03-color.md` §4, exactly.
277
+ */
278
+ export declare function renderColorFact(fact: ColorFact): string;
279
+ export interface ColorPair {
280
+ fg: string;
281
+ bgLabel: string;
282
+ ratio: number | null;
283
+ ratioReason: RatioReason | null;
284
+ elementIds: string[];
285
+ fontSizesPx: number[];
286
+ weights: number[];
287
+ /**
288
+ * The CSS `opacity` multiplier, present only when it is below 1 -- same
289
+ * meaning and same "present only below 1" convention as
290
+ * `ColorFact.effectiveOpacity`, taken from the first fact in the group
291
+ * exactly like `ratio`/`ratioReason` already are (elements sharing one
292
+ * (ink, backdrop) pair by construction share the composited result, and
293
+ * in the overwhelming common case share the same ancestor chain too).
294
+ *
295
+ * Added for A6: `q color`'s pair table used to have no way to carry this
296
+ * at all, so even a caller that *wanted* to show `opacity ×0` (or `×0.5`)
297
+ * next to a row -- the fact that explains a suspiciously round ratio like
298
+ * `1.00` on an ancestor-hidden element -- had nothing to read it from off
299
+ * a `ColorPair` directly. `query.ts`'s own fix for the same gap
300
+ * (`pairOpacities`) instead recovers the full, deduped set of a pair's
301
+ * member opacities from the ungrouped facts by `elementIds`, which this
302
+ * single first-fact value cannot replace when a pair's members disagree
303
+ * (rare, but possible) -- both are kept: this one for a caller that only
304
+ * needs "does this pair carry a reduced-opacity member" cheaply, without
305
+ * re-deriving a `factsById` map of its own.
306
+ */
307
+ effectiveOpacity: number | null;
308
+ }
309
+ /**
310
+ * One row per (ink, backdrop) pair, not per element -- "thousands of
311
+ * elements compress to dozens of rows" (design note §6). `fg` and the
312
+ * rendered `<bg>` form together are the grouping key: two elements with the
313
+ * same ink over the same backdrop are, by construction, the same measured
314
+ * ratio.
315
+ */
316
+ export declare function groupColorFacts(facts: ColorFact[]): ColorPair[];