@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,211 @@
1
+ /**
2
+ * The contract for the state diff.
3
+ *
4
+ * A diff answers "what changed between these two captures, and why" — a harder
5
+ * question than it sounds, because before anything can say `e12 moved down
6
+ * 21px` it has to know that `e12` in one capture and `e12` in the other are the
7
+ * same element, across a DOM that just changed.
8
+ *
9
+ * `identity.ts` and `match.ts` establish that. `changes.ts` turns matched pairs
10
+ * into facts and folds consequences under their causes. `render.ts` writes it
11
+ * down.
12
+ */
13
+ import type { ElementRecord, NotMeasured } from "../snapshot/types.cjs";
14
+ /**
15
+ * A capture indexed for matching.
16
+ *
17
+ * The derived values live in side maps rather than on the records. A bundle's
18
+ * elements belong to the caller, and the step-2 spike mutated them — which
19
+ * meant diffing the same bundle twice gave a different answer the second time.
20
+ */
21
+ export interface Prepared {
22
+ elements: readonly ElementRecord[];
23
+ byId: ReadonlyMap<string, ElementRecord>;
24
+ /** Position among siblings that produce the same tag. Identity rung 4. */
25
+ ordinal: ReadonlyMap<string, number>;
26
+ /**
27
+ * Inner text of the subtree, capped.
28
+ *
29
+ * Similo's "visible text" is inner text, not own text, and the distinction is
30
+ * load-bearing: a list item's own text is empty when its content sits in
31
+ * child spans, which is exactly the case the matcher has to get right. Own
32
+ * text alone leaves every `<li>` scoring identically and the match falls
33
+ * through to the sibling ordinal, which is the cascade the critique measured.
34
+ */
35
+ subtreeText: ReadonlyMap<string, string>;
36
+ /** Document order, so a renderer can print an act before its consequences. */
37
+ docIndex: ReadonlyMap<string, number>;
38
+ /** How many elements sit underneath. Used for label heuristics. */
39
+ descendantCount: ReadonlyMap<string, number>;
40
+ roots: readonly string[];
41
+ viewport: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /**
46
+ * From `Snapshot.truncated` (`src/snapshot/limits.ts`): this capture is a
47
+ * window bounded by the element cap, not the whole page. `match.ts` and
48
+ * `changes.ts` both read this rather than re-deriving it, so "truncated"
49
+ * means one thing everywhere a diff asks the question.
50
+ */
51
+ truncated: boolean;
52
+ }
53
+ /** before-id → after-id, and the inverse. */
54
+ export interface Match {
55
+ pairs: ReadonlyMap<string, string>;
56
+ reverse: ReadonlyMap<string, string>;
57
+ }
58
+ export interface BoxDelta {
59
+ dx: number;
60
+ dy: number;
61
+ dw: number;
62
+ dh: number;
63
+ }
64
+ export type ChangeKind = 'added' | 'removed' | 'appeared' | 'disappeared' | 'text' | 'attr' | 'style' | 'overflow' | 'clip' | 'box';
65
+ /**
66
+ * Why a change does not get a line of its own.
67
+ *
68
+ * `subsumed` means another line already says it: a `display: none → block` and
69
+ * a box arriving from `@0,0` are both the sentence "it appeared", and a
70
+ * `display:none` element's `@0,0` box is an artefact rather than an
71
+ * observation. `derived` means it is a genuine separate consequence, printed
72
+ * folded under the act that caused it.
73
+ *
74
+ * Nothing is dropped on a judgement about importance — only on a demonstrable
75
+ * causal relationship between two measurements already in hand.
76
+ */
77
+ export type Subsumption = 'visibility' | 'subtree';
78
+ export type Cause = 'primary' | 'derived';
79
+ export type Derivation = 'carried' | 'pushed' | 'absorbed' | 'reflowed' | 'inherited';
80
+ export interface Change {
81
+ kind: ChangeKind;
82
+ /**
83
+ * After-state id. Present on everything except `removed`, because the next
84
+ * thing a reader does is query the state they are now in.
85
+ */
86
+ afterId?: string;
87
+ beforeId?: string;
88
+ before?: ElementRecord | undefined;
89
+ after?: ElementRecord | undefined;
90
+ /** `style` only: the record property that changed, camelCase. */
91
+ prop?: string;
92
+ /** `style` only: whether this property can move or resize a box. */
93
+ geometric?: boolean;
94
+ /** `appeared` / `disappeared` only: `display:none` or `hidden`. */
95
+ reason?: string;
96
+ /** `overflow` only: whether the element overflows *now*. */
97
+ now?: boolean;
98
+ /** `box` only. */
99
+ delta?: BoxDelta;
100
+ /** Set by the subsumption pass in `changes.ts`. */
101
+ subsumed?: Subsumption;
102
+ subsumedBy?: string;
103
+ /** How many descendants folded into this one. */
104
+ descendants?: number;
105
+ cause?: Cause;
106
+ how?: Derivation;
107
+ /** The immediate cause's after-id. */
108
+ via?: string;
109
+ /**
110
+ * The cause at the end of the `via` chain, and its kind.
111
+ *
112
+ * Without this, a pushed row and the four words inside it become five groups
113
+ * that all mean "because e6 was inserted".
114
+ */
115
+ rootVia?: string;
116
+ rootHow?: Derivation;
117
+ /**
118
+ * `removed` / `added` only, and only when the capture whose window could
119
+ * have excluded the counterpart is truncated — for `removed`, the *after*
120
+ * capture; for `added`, the *before* capture. The precondition is per-kind,
121
+ * not "both captures truncated": a `removed` change only needs to ask
122
+ * whether the after capture is a window, because the before side already
123
+ * produced this element in hand regardless of its own truncation, and the
124
+ * mirror is true for `added`. Requiring both sides truncated used to
125
+ * silently drop this flag whenever the complete side happened to be the
126
+ * one whose window actually explains the gap.
127
+ *
128
+ * An element with no partner reads the same whether the page genuinely lost
129
+ * it or the truncation window simply no longer includes it — inserting one
130
+ * element earlier in the document shifts every later index, which can push
131
+ * real, still-present content past a fixed element cap. `changes.ts`'s
132
+ * `markBoundaryAmbiguous` tells the two apart using the document-order
133
+ * shift the matcher already measured on this element's nearest matched
134
+ * neighbour: when that shift would place its hypothetical counterpart
135
+ * outside the other capture's window, this is set instead of reporting a
136
+ * bare removal or addition. Never suppressed — see `render.ts`'s `?` line.
137
+ */
138
+ boundaryAmbiguous?: boolean;
139
+ /**
140
+ * The element count of the capture whose window boundary explains the
141
+ * ambiguity: for `removed`, the *after* capture (its cap is what could have
142
+ * cut the surviving counterpart off); for `added`, the *before* capture
143
+ * (its cap is what could have missed an element that was already there).
144
+ * `render.ts`'s `boundaryLine` must name the same capture this counts —
145
+ * printing this number under the other capture's label is F2.
146
+ */
147
+ capSize?: number;
148
+ }
149
+ export interface DiffOptions {
150
+ /**
151
+ * Maximum body lines. Default 15 — the diff-ack budget.
152
+ *
153
+ * A diff exists to collapse act-then-look into one round trip. One that costs
154
+ * more than re-reading the digest has defeated its own purpose.
155
+ */
156
+ maxLines?: number;
157
+ }
158
+ export interface DiffMeta {
159
+ /** What the two captures are called. */
160
+ before: string;
161
+ after: string;
162
+ viewportBefore: string;
163
+ viewportAfter: string;
164
+ /**
165
+ * From each side's `Snapshot.truncated`. Optional so a hand-built `DiffMeta`
166
+ * in a test fixture need not opine on it — the same posture
167
+ * `Snapshot.notMeasured` takes; `undefined` reads as "not truncated", never
168
+ * as "unknown", because both real capture paths always set the source
169
+ * `Snapshot.truncated` explicitly. `diffCaptures` always passes both.
170
+ */
171
+ truncatedBefore?: boolean;
172
+ truncatedAfter?: boolean;
173
+ /** Element count of each capture. Only used to word the truncation caveat. */
174
+ beforeCount?: number;
175
+ afterCount?: number;
176
+ /**
177
+ * Which capture path produced each side — `cdp` and `walk` do not see the
178
+ * same page on shadow DOM (`DigestMeta.strategy`'s doc comment), so a diff
179
+ * across the two is comparing capability, not just state. Optional for the
180
+ * same reason `truncatedBefore` is: a hand-built `DiffMeta` need not opine,
181
+ * and a `Snapshot` fixture with no `strategy` field carries none. Real
182
+ * captures (`CaptureResult`, `Bundle`) always have one, and `diffCaptures`
183
+ * forwards it structurally without the caller naming it explicitly.
184
+ */
185
+ strategyBefore?: 'cdp' | 'walk';
186
+ strategyAfter?: 'cdp' | 'walk';
187
+ /**
188
+ * Shadow DOM and iframe content each side walked past uncounted. Carried
189
+ * for `--json` and for `render.ts`'s `NOT MEASURED:` header line (B7 —
190
+ * this was measured and threaded all the way here, then rendered nowhere,
191
+ * for as long as the field existed) — the same structural-availability
192
+ * posture as `strategy` above. Rendering it makes no claim that two
193
+ * captures with different `notMeasured` are comparable or not, only that
194
+ * the fact is disclosed rather than silently dropped.
195
+ */
196
+ notMeasuredBefore?: NotMeasured;
197
+ notMeasuredAfter?: NotMeasured;
198
+ }
199
+ export interface DiffResult {
200
+ changes: readonly Change[];
201
+ text: string;
202
+ /**
203
+ * Whether either side of this diff is a window rather than a whole page —
204
+ * lifted out of `Snapshot.truncated` so a `--json` consumer learns it
205
+ * structurally, without re-parsing the prose caveat `text` carries.
206
+ */
207
+ truncated: {
208
+ before: boolean;
209
+ after: boolean;
210
+ };
211
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The contract for the state diff.
3
+ *
4
+ * A diff answers "what changed between these two captures, and why" — a harder
5
+ * question than it sounds, because before anything can say `e12 moved down
6
+ * 21px` it has to know that `e12` in one capture and `e12` in the other are the
7
+ * same element, across a DOM that just changed.
8
+ *
9
+ * `identity.ts` and `match.ts` establish that. `changes.ts` turns matched pairs
10
+ * into facts and folds consequences under their causes. `render.ts` writes it
11
+ * down.
12
+ */
13
+ import type { ElementRecord, NotMeasured } from "../snapshot/types.js";
14
+ /**
15
+ * A capture indexed for matching.
16
+ *
17
+ * The derived values live in side maps rather than on the records. A bundle's
18
+ * elements belong to the caller, and the step-2 spike mutated them — which
19
+ * meant diffing the same bundle twice gave a different answer the second time.
20
+ */
21
+ export interface Prepared {
22
+ elements: readonly ElementRecord[];
23
+ byId: ReadonlyMap<string, ElementRecord>;
24
+ /** Position among siblings that produce the same tag. Identity rung 4. */
25
+ ordinal: ReadonlyMap<string, number>;
26
+ /**
27
+ * Inner text of the subtree, capped.
28
+ *
29
+ * Similo's "visible text" is inner text, not own text, and the distinction is
30
+ * load-bearing: a list item's own text is empty when its content sits in
31
+ * child spans, which is exactly the case the matcher has to get right. Own
32
+ * text alone leaves every `<li>` scoring identically and the match falls
33
+ * through to the sibling ordinal, which is the cascade the critique measured.
34
+ */
35
+ subtreeText: ReadonlyMap<string, string>;
36
+ /** Document order, so a renderer can print an act before its consequences. */
37
+ docIndex: ReadonlyMap<string, number>;
38
+ /** How many elements sit underneath. Used for label heuristics. */
39
+ descendantCount: ReadonlyMap<string, number>;
40
+ roots: readonly string[];
41
+ viewport: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /**
46
+ * From `Snapshot.truncated` (`src/snapshot/limits.ts`): this capture is a
47
+ * window bounded by the element cap, not the whole page. `match.ts` and
48
+ * `changes.ts` both read this rather than re-deriving it, so "truncated"
49
+ * means one thing everywhere a diff asks the question.
50
+ */
51
+ truncated: boolean;
52
+ }
53
+ /** before-id → after-id, and the inverse. */
54
+ export interface Match {
55
+ pairs: ReadonlyMap<string, string>;
56
+ reverse: ReadonlyMap<string, string>;
57
+ }
58
+ export interface BoxDelta {
59
+ dx: number;
60
+ dy: number;
61
+ dw: number;
62
+ dh: number;
63
+ }
64
+ export type ChangeKind = 'added' | 'removed' | 'appeared' | 'disappeared' | 'text' | 'attr' | 'style' | 'overflow' | 'clip' | 'box';
65
+ /**
66
+ * Why a change does not get a line of its own.
67
+ *
68
+ * `subsumed` means another line already says it: a `display: none → block` and
69
+ * a box arriving from `@0,0` are both the sentence "it appeared", and a
70
+ * `display:none` element's `@0,0` box is an artefact rather than an
71
+ * observation. `derived` means it is a genuine separate consequence, printed
72
+ * folded under the act that caused it.
73
+ *
74
+ * Nothing is dropped on a judgement about importance — only on a demonstrable
75
+ * causal relationship between two measurements already in hand.
76
+ */
77
+ export type Subsumption = 'visibility' | 'subtree';
78
+ export type Cause = 'primary' | 'derived';
79
+ export type Derivation = 'carried' | 'pushed' | 'absorbed' | 'reflowed' | 'inherited';
80
+ export interface Change {
81
+ kind: ChangeKind;
82
+ /**
83
+ * After-state id. Present on everything except `removed`, because the next
84
+ * thing a reader does is query the state they are now in.
85
+ */
86
+ afterId?: string;
87
+ beforeId?: string;
88
+ before?: ElementRecord | undefined;
89
+ after?: ElementRecord | undefined;
90
+ /** `style` only: the record property that changed, camelCase. */
91
+ prop?: string;
92
+ /** `style` only: whether this property can move or resize a box. */
93
+ geometric?: boolean;
94
+ /** `appeared` / `disappeared` only: `display:none` or `hidden`. */
95
+ reason?: string;
96
+ /** `overflow` only: whether the element overflows *now*. */
97
+ now?: boolean;
98
+ /** `box` only. */
99
+ delta?: BoxDelta;
100
+ /** Set by the subsumption pass in `changes.ts`. */
101
+ subsumed?: Subsumption;
102
+ subsumedBy?: string;
103
+ /** How many descendants folded into this one. */
104
+ descendants?: number;
105
+ cause?: Cause;
106
+ how?: Derivation;
107
+ /** The immediate cause's after-id. */
108
+ via?: string;
109
+ /**
110
+ * The cause at the end of the `via` chain, and its kind.
111
+ *
112
+ * Without this, a pushed row and the four words inside it become five groups
113
+ * that all mean "because e6 was inserted".
114
+ */
115
+ rootVia?: string;
116
+ rootHow?: Derivation;
117
+ /**
118
+ * `removed` / `added` only, and only when the capture whose window could
119
+ * have excluded the counterpart is truncated — for `removed`, the *after*
120
+ * capture; for `added`, the *before* capture. The precondition is per-kind,
121
+ * not "both captures truncated": a `removed` change only needs to ask
122
+ * whether the after capture is a window, because the before side already
123
+ * produced this element in hand regardless of its own truncation, and the
124
+ * mirror is true for `added`. Requiring both sides truncated used to
125
+ * silently drop this flag whenever the complete side happened to be the
126
+ * one whose window actually explains the gap.
127
+ *
128
+ * An element with no partner reads the same whether the page genuinely lost
129
+ * it or the truncation window simply no longer includes it — inserting one
130
+ * element earlier in the document shifts every later index, which can push
131
+ * real, still-present content past a fixed element cap. `changes.ts`'s
132
+ * `markBoundaryAmbiguous` tells the two apart using the document-order
133
+ * shift the matcher already measured on this element's nearest matched
134
+ * neighbour: when that shift would place its hypothetical counterpart
135
+ * outside the other capture's window, this is set instead of reporting a
136
+ * bare removal or addition. Never suppressed — see `render.ts`'s `?` line.
137
+ */
138
+ boundaryAmbiguous?: boolean;
139
+ /**
140
+ * The element count of the capture whose window boundary explains the
141
+ * ambiguity: for `removed`, the *after* capture (its cap is what could have
142
+ * cut the surviving counterpart off); for `added`, the *before* capture
143
+ * (its cap is what could have missed an element that was already there).
144
+ * `render.ts`'s `boundaryLine` must name the same capture this counts —
145
+ * printing this number under the other capture's label is F2.
146
+ */
147
+ capSize?: number;
148
+ }
149
+ export interface DiffOptions {
150
+ /**
151
+ * Maximum body lines. Default 15 — the diff-ack budget.
152
+ *
153
+ * A diff exists to collapse act-then-look into one round trip. One that costs
154
+ * more than re-reading the digest has defeated its own purpose.
155
+ */
156
+ maxLines?: number;
157
+ }
158
+ export interface DiffMeta {
159
+ /** What the two captures are called. */
160
+ before: string;
161
+ after: string;
162
+ viewportBefore: string;
163
+ viewportAfter: string;
164
+ /**
165
+ * From each side's `Snapshot.truncated`. Optional so a hand-built `DiffMeta`
166
+ * in a test fixture need not opine on it — the same posture
167
+ * `Snapshot.notMeasured` takes; `undefined` reads as "not truncated", never
168
+ * as "unknown", because both real capture paths always set the source
169
+ * `Snapshot.truncated` explicitly. `diffCaptures` always passes both.
170
+ */
171
+ truncatedBefore?: boolean;
172
+ truncatedAfter?: boolean;
173
+ /** Element count of each capture. Only used to word the truncation caveat. */
174
+ beforeCount?: number;
175
+ afterCount?: number;
176
+ /**
177
+ * Which capture path produced each side — `cdp` and `walk` do not see the
178
+ * same page on shadow DOM (`DigestMeta.strategy`'s doc comment), so a diff
179
+ * across the two is comparing capability, not just state. Optional for the
180
+ * same reason `truncatedBefore` is: a hand-built `DiffMeta` need not opine,
181
+ * and a `Snapshot` fixture with no `strategy` field carries none. Real
182
+ * captures (`CaptureResult`, `Bundle`) always have one, and `diffCaptures`
183
+ * forwards it structurally without the caller naming it explicitly.
184
+ */
185
+ strategyBefore?: 'cdp' | 'walk';
186
+ strategyAfter?: 'cdp' | 'walk';
187
+ /**
188
+ * Shadow DOM and iframe content each side walked past uncounted. Carried
189
+ * for `--json` and for `render.ts`'s `NOT MEASURED:` header line (B7 —
190
+ * this was measured and threaded all the way here, then rendered nowhere,
191
+ * for as long as the field existed) — the same structural-availability
192
+ * posture as `strategy` above. Rendering it makes no claim that two
193
+ * captures with different `notMeasured` are comparable or not, only that
194
+ * the fact is disclosed rather than silently dropped.
195
+ */
196
+ notMeasuredBefore?: NotMeasured;
197
+ notMeasuredAfter?: NotMeasured;
198
+ }
199
+ export interface DiffResult {
200
+ changes: readonly Change[];
201
+ text: string;
202
+ /**
203
+ * Whether either side of this diff is a window rather than a whole page —
204
+ * lifted out of `Snapshot.truncated` so a `--json` consumer learns it
205
+ * structurally, without re-parsing the prose caveat `text` carries.
206
+ */
207
+ truncated: {
208
+ before: boolean;
209
+ after: boolean;
210
+ };
211
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SPARSE_CAPTURE_MARKER = exports.renderDigest = exports.parseDigestLine = void 0;
4
+ var parse_js_1 = require("./parse.cjs");
5
+ Object.defineProperty(exports, "parseDigestLine", { enumerable: true, get: function () { return parse_js_1.parseDigestLine; } });
6
+ var render_js_1 = require("./render.cjs");
7
+ Object.defineProperty(exports, "renderDigest", { enumerable: true, get: function () { return render_js_1.renderDigest; } });
8
+ Object.defineProperty(exports, "SPARSE_CAPTURE_MARKER", { enumerable: true, get: function () { return render_js_1.SPARSE_CAPTURE_MARKER; } });
@@ -0,0 +1,4 @@
1
+ export type { ParsedCollapsedLine, ParsedElementLine, ParsedFoldEntry, ParsedFoldLine, ParsedGap, ParsedLine, } from "./parse.cjs";
2
+ export { parseDigestLine } from "./parse.cjs";
3
+ export { renderDigest, SPARSE_CAPTURE_MARKER } from "./render.cjs";
4
+ export type { DigestMeta, RenderDigestOptions } from "./types.cjs";
@@ -0,0 +1,4 @@
1
+ export type { ParsedCollapsedLine, ParsedElementLine, ParsedFoldEntry, ParsedFoldLine, ParsedGap, ParsedLine, } from "./parse.js";
2
+ export { parseDigestLine } from "./parse.js";
3
+ export { renderDigest, SPARSE_CAPTURE_MARKER } from "./render.js";
4
+ export type { DigestMeta, RenderDigestOptions } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { parseDigestLine } from "./parse.js";
2
+ export { renderDigest, SPARSE_CAPTURE_MARKER } from "./render.js";
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Parses one line of a rendered digest back into structured data. This is what
4
+ * makes the format machine-readable as well as agent-readable: a consumer that
5
+ * wants numbers rather than text does not have to re-implement the grammar.
6
+ *
7
+ * Ported from the prototype, where it round-tripped every golden digest, plus
8
+ * the v2 addition: facts.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.parseDigestLine = parseDigestLine;
12
+ const GAP_RE = /([↓→])(-?\d+)( OVERLAP)?/;
13
+ const POS_SIZE_RE = /@(-?\d+),(-?\d+)\s+(\d+)x(\d+)/;
14
+ function parseGapToken(token) {
15
+ if (!token)
16
+ return null;
17
+ const match = GAP_RE.exec(token);
18
+ if (!match)
19
+ return null;
20
+ const [, arrow, valueStr, overlapStr] = match;
21
+ return {
22
+ direction: arrow === '↓' ? 'down' : 'right',
23
+ value: Number(valueStr),
24
+ overlap: Boolean(overlapStr),
25
+ };
26
+ }
27
+ /** A fold line's per-entry list: `id @x,y [gap], id @x,y [gap], ...`. */
28
+ function parseFoldEntries(list) {
29
+ const entries = [];
30
+ // Entries never contain a literal ", " -- ids, positions and gaps cannot --
31
+ // so a plain split is safe.
32
+ for (const part of list.split(', ')) {
33
+ const match = /^(\S+)\s+@(-?\d+),(-?\d+)(.*)$/.exec(part.trim());
34
+ if (!match)
35
+ continue;
36
+ const [, id, xStr, yStr, rest] = match;
37
+ if (!id || xStr === undefined || yStr === undefined)
38
+ continue;
39
+ entries.push({ id, x: Number(xStr), y: Number(yStr), gap: parseGapToken(rest ?? null) });
40
+ }
41
+ return entries;
42
+ }
43
+ /**
44
+ * Every `[...]` group after the geometry that is not a known structural token.
45
+ * Facts are open-ended by design — the renderer must be able to add one without
46
+ * the parser needing to know its name first.
47
+ */
48
+ function parseFacts(after) {
49
+ const structural = /^(row|column|grid|below fold|wraps \d+)$/;
50
+ const facts = [];
51
+ for (const match of after.matchAll(/\[([^\]]+)\]/g)) {
52
+ const body = match[1];
53
+ if (body && !structural.test(body))
54
+ facts.push(body);
55
+ }
56
+ return facts;
57
+ }
58
+ /** Comment (`#…`) and blank lines carry no per-node data and parse to `null`. */
59
+ function parseDigestLine(line) {
60
+ const trimmed = line.trim();
61
+ if (trimmed === '' || trimmed.startsWith('#'))
62
+ return null;
63
+ const depth = Math.floor((line.length - line.trimStart().length) / 2);
64
+ // The `, <K> facts` clause is optional -- present only when the collapsed
65
+ // subtree hides at least one fact-carrying element (`digest/render.ts`'s
66
+ // `renderChildren`) -- so it is its own optional group rather than folded
67
+ // into a catch-all, the same discipline the fold line's variance token
68
+ // uses. `(--focus <id>)` is not optional in what the renderer emits (every
69
+ // `[folded: …]` line carries it -- P1-1: the collapse line used to be the
70
+ // one place in the whole format that disclosed the most while pointing
71
+ // nowhere), but the group here is still `?` so a hand-built or
72
+ // differently-versioned line missing it still parses rather than going
73
+ // `null`.
74
+ const collapsed = /^\[folded:\s*(\d+)\s*nodes(?:,\s*(\d+)\s*facts)?,\s*bbox\s*@(-?\d+),(-?\d+)\s+(\d+)x(\d+)(?:\s*\(--focus\s+(\S+)\))?\]$/.exec(trimmed);
75
+ if (collapsed) {
76
+ const [, count, facts, x, y, w, h, focusHint] = collapsed;
77
+ return {
78
+ kind: 'collapsed',
79
+ nodeCount: Number(count),
80
+ factCount: facts ? Number(facts) : 0,
81
+ bbox: { x: Number(x), y: Number(y), width: Number(w), height: Number(h) },
82
+ focusHint: focusHint ?? '',
83
+ };
84
+ }
85
+ // `×N[/period] more like <id>[ (variance)] — entries`. The variance and
86
+ // period groups are both explicit and optional rather than folded into a
87
+ // catch-all `\S+` for `likeId` -- a variance token has a space in it
88
+ // (`r0 (h 30-90)`), and `(\S+)\s*—` alone cannot span that, which used to
89
+ // make every folded id on a page with a size spread unparseable.
90
+ const fold = /^\[fold:\s*×(\d+)(?:\/(\d+))?\s*more like (\S+)(?:\s*(\([^()]*\)))?\s*—\s*(.+)\]$/.exec(trimmed);
91
+ if (fold) {
92
+ const [, count, period, likeId, variance, list] = fold;
93
+ if (!likeId || !list)
94
+ return null;
95
+ return {
96
+ kind: 'fold',
97
+ count: Number(count),
98
+ likeId,
99
+ period: period ? Number(period) : 1,
100
+ variance: variance ?? null,
101
+ entries: parseFoldEntries(list),
102
+ };
103
+ }
104
+ const posSize = POS_SIZE_RE.exec(trimmed);
105
+ if (!posSize)
106
+ return null;
107
+ const [, xStr, yStr, wStr, hStr] = posSize;
108
+ if (xStr === undefined || yStr === undefined || wStr === undefined || hStr === undefined) {
109
+ return null;
110
+ }
111
+ const before = trimmed.slice(0, posSize.index).trim();
112
+ const after = trimmed.slice(posSize.index + posSize[0].length);
113
+ // Text is split off first, then id/tag/annotation are parsed from what's
114
+ // left -- not one combined regex -- because the renderer does not escape
115
+ // the text it quotes, and a real accessible name can contain a literal
116
+ // `"` (ar.wikipedia.org link titles quote a word inline: `"الملحوظة"`
117
+ // inside the name). `(.*?)` is lazy, so it finds the *first* `\s+"` in
118
+ // `before` -- right after tag/annotation, since neither ever contains
119
+ // `\s+"` itself -- and the greedy `(.*)"$` then takes everything from
120
+ // there to the final `"` at the very end of the line as the text,
121
+ // embedded quotes included. Without this split, a naive `"([^"]*)"$`
122
+ // stops at the first embedded quote and the anchored `$` never matches,
123
+ // so the whole line -- id, tag, position, every fact on it -- was
124
+ // discarded.
125
+ const textMatch = /^(.*?)\s+"(.*)"$/.exec(before);
126
+ const head = textMatch ? textMatch[1] : before;
127
+ const text = textMatch ? textMatch[2] : undefined;
128
+ if (head === undefined)
129
+ return null;
130
+ // Tag: an optional `::` pseudo-element prefix (`::before`, `::after`,
131
+ // `::marker` -- real tokens on a real capture; the CDP snapshot path
132
+ // reports a pseudo-element's node name verbatim) followed by a plain tag
133
+ // name, which on a modern page is routinely a hyphenated custom-element
134
+ // name (`sl-button`, or any other web component). Annotation, when
135
+ // present, is greedy (`.*`) rather than `[^\s"]+` from its `.`/`#` sigil to
136
+ // the end of what's left: a role (`.heading`) never has whitespace in it,
137
+ // but a `data-testid` value is raw attribute text, and real pages put
138
+ // whitespace in it -- e.g. grafana.com renders
139
+ // `data-testid="data-testid navigation mega-menu"` verbatim, so the
140
+ // annotation itself is `data-testid navigation mega-menu`. The text (if
141
+ // any) is already stripped off above, so there is nothing after the
142
+ // annotation left to over-consume.
143
+ const idTagMatch = /^(\S+)\s+((?:::)?[a-zA-Z][a-zA-Z0-9-]*)([.#].*)?$/.exec(head);
144
+ if (!idTagMatch)
145
+ return null;
146
+ const [, id, tag, annotation] = idTagMatch;
147
+ if (!id || !tag)
148
+ return null;
149
+ const flow = /\[(row|column|grid)\]/.exec(after);
150
+ return {
151
+ kind: 'element',
152
+ id,
153
+ tag,
154
+ annotation: annotation ? annotation.slice(1) : null,
155
+ text: text ?? null,
156
+ rect: { x: Number(xStr), y: Number(yStr), width: Number(wStr), height: Number(hStr) },
157
+ flow: flow?.[1] ?? 'none',
158
+ gap: parseGapToken(GAP_RE.exec(after)?.[0] ?? null),
159
+ facts: parseFacts(after),
160
+ belowFold: /\[below fold\]/.test(after),
161
+ depth,
162
+ };
163
+ }