@michaelmagan/dnumb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/analysis/cluster.cjs +114 -0
- package/dist/analysis/cluster.d.cts +68 -0
- package/dist/analysis/cluster.d.ts +68 -0
- package/dist/analysis/cluster.js +109 -0
- package/dist/analysis/distances.cjs +124 -0
- package/dist/analysis/distances.d.cts +58 -0
- package/dist/analysis/distances.d.ts +58 -0
- package/dist/analysis/distances.js +119 -0
- package/dist/analysis/geometry.cjs +60 -0
- package/dist/analysis/geometry.d.cts +34 -0
- package/dist/analysis/geometry.d.ts +34 -0
- package/dist/analysis/geometry.js +53 -0
- package/dist/analysis/layout-tree.cjs +245 -0
- package/dist/analysis/layout-tree.d.cts +45 -0
- package/dist/analysis/layout-tree.d.ts +45 -0
- package/dist/analysis/layout-tree.js +242 -0
- package/dist/analysis/region-query.cjs +71 -0
- package/dist/analysis/region-query.d.cts +25 -0
- package/dist/analysis/region-query.d.ts +25 -0
- package/dist/analysis/region-query.js +67 -0
- package/dist/analysis/types.cjs +7 -0
- package/dist/analysis/types.d.cts +143 -0
- package/dist/analysis/types.d.ts +143 -0
- package/dist/analysis/types.js +6 -0
- package/dist/bundle.cjs +182 -0
- package/dist/bundle.d.cts +91 -0
- package/dist/bundle.d.ts +91 -0
- package/dist/bundle.js +175 -0
- package/dist/cli.cjs +755 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +719 -0
- package/dist/diff/changes.cjs +656 -0
- package/dist/diff/changes.d.cts +2 -0
- package/dist/diff/changes.d.ts +2 -0
- package/dist/diff/changes.js +653 -0
- package/dist/diff/identity.cjs +167 -0
- package/dist/diff/identity.d.cts +62 -0
- package/dist/diff/identity.d.ts +62 -0
- package/dist/diff/identity.js +159 -0
- package/dist/diff/index.cjs +84 -0
- package/dist/diff/index.d.cts +76 -0
- package/dist/diff/index.d.ts +76 -0
- package/dist/diff/index.js +68 -0
- package/dist/diff/match.cjs +156 -0
- package/dist/diff/match.d.cts +17 -0
- package/dist/diff/match.d.ts +17 -0
- package/dist/diff/match.js +152 -0
- package/dist/diff/render.cjs +540 -0
- package/dist/diff/render.d.cts +2 -0
- package/dist/diff/render.d.ts +2 -0
- package/dist/diff/render.js +537 -0
- package/dist/diff/types.cjs +2 -0
- package/dist/diff/types.d.cts +211 -0
- package/dist/diff/types.d.ts +211 -0
- package/dist/diff/types.js +1 -0
- package/dist/digest/index.cjs +8 -0
- package/dist/digest/index.d.cts +4 -0
- package/dist/digest/index.d.ts +4 -0
- package/dist/digest/index.js +2 -0
- package/dist/digest/parse.cjs +163 -0
- package/dist/digest/parse.d.cts +80 -0
- package/dist/digest/parse.d.ts +80 -0
- package/dist/digest/parse.js +160 -0
- package/dist/digest/render.cjs +1575 -0
- package/dist/digest/render.d.cts +41 -0
- package/dist/digest/render.d.ts +41 -0
- package/dist/digest/render.js +1571 -0
- package/dist/digest/types.cjs +10 -0
- package/dist/digest/types.d.cts +86 -0
- package/dist/digest/types.d.ts +86 -0
- package/dist/digest/types.js +9 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +19 -0
- package/dist/page.cjs +35 -0
- package/dist/page.d.cts +40 -0
- package/dist/page.d.ts +40 -0
- package/dist/page.js +31 -0
- package/dist/playwright.cjs +138 -0
- package/dist/playwright.d.cts +69 -0
- package/dist/playwright.d.ts +69 -0
- package/dist/playwright.js +131 -0
- package/dist/query.cjs +1152 -0
- package/dist/query.d.cts +47 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +1146 -0
- package/dist/snapshot/capture.cjs +80 -0
- package/dist/snapshot/capture.d.cts +34 -0
- package/dist/snapshot/capture.d.ts +34 -0
- package/dist/snapshot/capture.js +77 -0
- package/dist/snapshot/cdp.cjs +450 -0
- package/dist/snapshot/cdp.d.cts +36 -0
- package/dist/snapshot/cdp.d.ts +36 -0
- package/dist/snapshot/cdp.js +447 -0
- package/dist/snapshot/clip.cjs +100 -0
- package/dist/snapshot/clip.d.cts +26 -0
- package/dist/snapshot/clip.d.ts +26 -0
- package/dist/snapshot/clip.js +96 -0
- package/dist/snapshot/collect.cjs +358 -0
- package/dist/snapshot/collect.d.cts +18 -0
- package/dist/snapshot/collect.d.ts +18 -0
- package/dist/snapshot/collect.js +355 -0
- package/dist/snapshot/color.cjs +1039 -0
- package/dist/snapshot/color.d.cts +316 -0
- package/dist/snapshot/color.d.ts +316 -0
- package/dist/snapshot/color.js +1026 -0
- package/dist/snapshot/facts.cjs +167 -0
- package/dist/snapshot/facts.d.cts +48 -0
- package/dist/snapshot/facts.d.ts +48 -0
- package/dist/snapshot/facts.js +161 -0
- package/dist/snapshot/limits.cjs +69 -0
- package/dist/snapshot/limits.d.cts +59 -0
- package/dist/snapshot/limits.d.ts +59 -0
- package/dist/snapshot/limits.js +65 -0
- package/dist/snapshot/not-measured.cjs +44 -0
- package/dist/snapshot/not-measured.d.cts +14 -0
- package/dist/snapshot/not-measured.d.ts +14 -0
- package/dist/snapshot/not-measured.js +41 -0
- package/dist/snapshot/types.cjs +2 -0
- package/dist/snapshot/types.d.cts +238 -0
- package/dist/snapshot/types.d.ts +238 -0
- package/dist/snapshot/types.js +1 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +15 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.js +6 -0
- package/package.json +141 -0
- package/skills/dnumb/SKILL.md +136 -0
|
@@ -0,0 +1,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,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
|
+
}
|