@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,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MATCH_THRESHOLD = exports.WEAK_ROLES = void 0;
|
|
4
|
+
exports.isGeneratedId = isGeneratedId;
|
|
5
|
+
exports.hardKey = hardKey;
|
|
6
|
+
exports.textSimilarity = textSimilarity;
|
|
7
|
+
exports.similarity = similarity;
|
|
8
|
+
exports.geometryTiebreak = geometryTiebreak;
|
|
9
|
+
/** qain's set, verbatim. Roles too generic to identify anything. */
|
|
10
|
+
exports.WEAK_ROLES = new Set([
|
|
11
|
+
'generic',
|
|
12
|
+
'none',
|
|
13
|
+
'presentation',
|
|
14
|
+
'GenericContainer',
|
|
15
|
+
'InlineTextBox',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Ids that differ between two renders of the identical page.
|
|
19
|
+
*
|
|
20
|
+
* No source in the step-1 prior-art read has this guard: qain trusts `#id`
|
|
21
|
+
* unconditionally, and browser-use's dynamic filter covers classes only. React
|
|
22
|
+
* 18's `useId`, Radix, Headless UI, MUI, Emotion, styled-components and Angular
|
|
23
|
+
* all emit ids that change on every render, and treating one as a stable key
|
|
24
|
+
* makes an unchanged element look replaced.
|
|
25
|
+
*/
|
|
26
|
+
const GENERATED_ID = [
|
|
27
|
+
/:/, // React 18 useId -> ":r0:", ":R1mcq:"; Radix and Headless UI wrap it
|
|
28
|
+
/^mui-\d+$/,
|
|
29
|
+
/^ember\d+$/,
|
|
30
|
+
/^ng-tns-c\d+-\d+$/,
|
|
31
|
+
/^_ngcontent-\w+-c\d+$/,
|
|
32
|
+
/^css-[0-9a-z]{6,}$/i,
|
|
33
|
+
/^sc-[A-Za-z]{6,}$/,
|
|
34
|
+
/^[0-9a-f]{8,}$/i, // a bare hex or uuid run
|
|
35
|
+
];
|
|
36
|
+
function isGeneratedId(id) {
|
|
37
|
+
return GENERATED_ID.some((pattern) => pattern.test(id));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rungs 1 and 2: a key identifying the element wherever it sits.
|
|
41
|
+
*
|
|
42
|
+
* `null` when the element offers nothing document-unique and trustworthy, in
|
|
43
|
+
* which case it falls through to the scored rungs.
|
|
44
|
+
*/
|
|
45
|
+
function hardKey(el) {
|
|
46
|
+
if (el.testId)
|
|
47
|
+
return `@${el.testId}`;
|
|
48
|
+
if (el.domId && !isGeneratedId(el.domId))
|
|
49
|
+
return `#${el.domId}`;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Feature weights.
|
|
54
|
+
*
|
|
55
|
+
* Similo's table puts location, area and shape in its lowest tier (0.5) even
|
|
56
|
+
* for its own task, which is finding an element on a page that is *supposed* to
|
|
57
|
+
* be unchanged. Here they are absent entirely; the rest is scaled around role
|
|
58
|
+
* and accessible name being the strongest available evidence of sameness.
|
|
59
|
+
*/
|
|
60
|
+
const W = {
|
|
61
|
+
role: 2.0,
|
|
62
|
+
name: 2.0,
|
|
63
|
+
tag: 1.5,
|
|
64
|
+
ownText: 1.0,
|
|
65
|
+
subtreeText: 1.0,
|
|
66
|
+
ordinal: 0.5,
|
|
67
|
+
};
|
|
68
|
+
const normalise = (s) => (s ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
69
|
+
/**
|
|
70
|
+
* Levenshtein similarity in [0, 1], or `null` when neither side has the feature
|
|
71
|
+
* at all — which is different from both sides having it and disagreeing, and
|
|
72
|
+
* has to be, or two elements with no accessible name would be punished for it.
|
|
73
|
+
*
|
|
74
|
+
* Inputs are capped at 80 characters upstream, so O(n·m) is fine.
|
|
75
|
+
*/
|
|
76
|
+
function textSimilarity(a, b) {
|
|
77
|
+
const x = normalise(a);
|
|
78
|
+
const y = normalise(b);
|
|
79
|
+
if (!x && !y)
|
|
80
|
+
return null;
|
|
81
|
+
if (!x || !y)
|
|
82
|
+
return 0;
|
|
83
|
+
if (x === y)
|
|
84
|
+
return 1;
|
|
85
|
+
const m = x.length;
|
|
86
|
+
const n = y.length;
|
|
87
|
+
let previous = Array.from({ length: n + 1 }, (_, j) => j);
|
|
88
|
+
for (let i = 1; i <= m; i++) {
|
|
89
|
+
const current = [i];
|
|
90
|
+
for (let j = 1; j <= n; j++) {
|
|
91
|
+
const substitution = (previous[j - 1] ?? 0) + (x[i - 1] === y[j - 1] ? 0 : 1);
|
|
92
|
+
current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution);
|
|
93
|
+
}
|
|
94
|
+
previous = current;
|
|
95
|
+
}
|
|
96
|
+
return 1 - (previous[n] ?? 0) / Math.max(m, n);
|
|
97
|
+
}
|
|
98
|
+
const usableRole = (el) => el.role && !exports.WEAK_ROLES.has(el.role) ? el.role : null;
|
|
99
|
+
/**
|
|
100
|
+
* Rungs 3 and 4, as a normalised score in [0, 1].
|
|
101
|
+
*
|
|
102
|
+
* The denominator is the sum of weights of *applicable* features — those where
|
|
103
|
+
* at least one side has a value. Two bare divs therefore score 1.0 on tag and
|
|
104
|
+
* ordinal alone rather than being punished for having no role or name.
|
|
105
|
+
*/
|
|
106
|
+
function similarity(a, b, input) {
|
|
107
|
+
let got = 0;
|
|
108
|
+
let possible = 0;
|
|
109
|
+
const roleA = usableRole(a);
|
|
110
|
+
const roleB = usableRole(b);
|
|
111
|
+
if (roleA || roleB) {
|
|
112
|
+
possible += W.role;
|
|
113
|
+
if (roleA && roleA === roleB)
|
|
114
|
+
got += W.role;
|
|
115
|
+
}
|
|
116
|
+
const nameSimilarity = textSimilarity(a.name, b.name);
|
|
117
|
+
if (nameSimilarity !== null) {
|
|
118
|
+
possible += W.name;
|
|
119
|
+
got += W.name * nameSimilarity;
|
|
120
|
+
}
|
|
121
|
+
possible += W.tag;
|
|
122
|
+
if (a.tag === b.tag)
|
|
123
|
+
got += W.tag;
|
|
124
|
+
const ownSimilarity = textSimilarity(a.text, b.text);
|
|
125
|
+
if (ownSimilarity !== null) {
|
|
126
|
+
possible += W.ownText;
|
|
127
|
+
got += W.ownText * ownSimilarity;
|
|
128
|
+
}
|
|
129
|
+
// Subtree text is a weighted feature with a graded comparator, never part of
|
|
130
|
+
// a key, and it never propagates into a parent's identity. That is the whole
|
|
131
|
+
// difference between it and the text-hash approach the critique showed
|
|
132
|
+
// cascading: there, one child's text edit re-keyed its ancestors.
|
|
133
|
+
const subtreeSimilarity = textSimilarity(input.subtreeTextA, input.subtreeTextB);
|
|
134
|
+
if (subtreeSimilarity !== null) {
|
|
135
|
+
possible += W.subtreeText;
|
|
136
|
+
got += W.subtreeText * subtreeSimilarity;
|
|
137
|
+
}
|
|
138
|
+
possible += W.ordinal;
|
|
139
|
+
if (input.ordinalA === input.ordinalB)
|
|
140
|
+
got += W.ordinal;
|
|
141
|
+
return possible === 0 ? 0 : got / possible;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A tiebreak, and only a tiebreak.
|
|
145
|
+
*
|
|
146
|
+
* Geometry is the payload being diffed. Score it and an element that moved far
|
|
147
|
+
* enough scores worse against itself than against some stationary neighbour, so
|
|
148
|
+
* the diff reports a removal plus an addition — erasing the single line the
|
|
149
|
+
* whole tool exists to print, and doing it *more* the bigger the layout change
|
|
150
|
+
* is. That is the wrong direction on every axis.
|
|
151
|
+
*
|
|
152
|
+
* Scaled to 1e-6 so it can only order candidates that already scored equal —
|
|
153
|
+
* three identical Delete buttons in a table row, where position is genuinely
|
|
154
|
+
* the only discriminator — and can never outvote role and name.
|
|
155
|
+
*/
|
|
156
|
+
function geometryTiebreak(a, b) {
|
|
157
|
+
const dx = a.rect.x - b.rect.x;
|
|
158
|
+
const dy = a.rect.y - b.rect.y;
|
|
159
|
+
return 1e-6 / (1 + Math.sqrt(dx * dx + dy * dy));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Below this, two elements are not the same thing. Deliberately permissive:
|
|
163
|
+
* a missed match costs a phantom add plus a phantom remove, which is two wrong
|
|
164
|
+
* lines, while a wrong match costs one wrong line. The renderer can survive a
|
|
165
|
+
* loose match; it cannot reconstruct a missed one.
|
|
166
|
+
*/
|
|
167
|
+
exports.MATCH_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element identity across two captures. The ladder from
|
|
3
|
+
* `docs/notes/01-identity-ladder.md`, made executable.
|
|
4
|
+
*
|
|
5
|
+
* Two hard rungs resolved before any scoring, then a scored match. The
|
|
6
|
+
* counter-intuitive part, and the one thing here most likely to be "fixed" back
|
|
7
|
+
* into a bug: **geometry is never a scoring feature.** See `geometryTiebreak`.
|
|
8
|
+
*/
|
|
9
|
+
import type { ElementRecord } from "../snapshot/types.cjs";
|
|
10
|
+
/** qain's set, verbatim. Roles too generic to identify anything. */
|
|
11
|
+
export declare const WEAK_ROLES: ReadonlySet<string>;
|
|
12
|
+
export declare function isGeneratedId(id: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Rungs 1 and 2: a key identifying the element wherever it sits.
|
|
15
|
+
*
|
|
16
|
+
* `null` when the element offers nothing document-unique and trustworthy, in
|
|
17
|
+
* which case it falls through to the scored rungs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hardKey(el: ElementRecord): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Levenshtein similarity in [0, 1], or `null` when neither side has the feature
|
|
22
|
+
* at all — which is different from both sides having it and disagreeing, and
|
|
23
|
+
* has to be, or two elements with no accessible name would be punished for it.
|
|
24
|
+
*
|
|
25
|
+
* Inputs are capped at 80 characters upstream, so O(n·m) is fine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function textSimilarity(a: string | null | undefined, b: string | null | undefined): number | null;
|
|
28
|
+
export interface SimilarityInput {
|
|
29
|
+
ordinalA: number | undefined;
|
|
30
|
+
ordinalB: number | undefined;
|
|
31
|
+
subtreeTextA: string | undefined;
|
|
32
|
+
subtreeTextB: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rungs 3 and 4, as a normalised score in [0, 1].
|
|
36
|
+
*
|
|
37
|
+
* The denominator is the sum of weights of *applicable* features — those where
|
|
38
|
+
* at least one side has a value. Two bare divs therefore score 1.0 on tag and
|
|
39
|
+
* ordinal alone rather than being punished for having no role or name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function similarity(a: ElementRecord, b: ElementRecord, input: SimilarityInput): number;
|
|
42
|
+
/**
|
|
43
|
+
* A tiebreak, and only a tiebreak.
|
|
44
|
+
*
|
|
45
|
+
* Geometry is the payload being diffed. Score it and an element that moved far
|
|
46
|
+
* enough scores worse against itself than against some stationary neighbour, so
|
|
47
|
+
* the diff reports a removal plus an addition — erasing the single line the
|
|
48
|
+
* whole tool exists to print, and doing it *more* the bigger the layout change
|
|
49
|
+
* is. That is the wrong direction on every axis.
|
|
50
|
+
*
|
|
51
|
+
* Scaled to 1e-6 so it can only order candidates that already scored equal —
|
|
52
|
+
* three identical Delete buttons in a table row, where position is genuinely
|
|
53
|
+
* the only discriminator — and can never outvote role and name.
|
|
54
|
+
*/
|
|
55
|
+
export declare function geometryTiebreak(a: ElementRecord, b: ElementRecord): number;
|
|
56
|
+
/**
|
|
57
|
+
* Below this, two elements are not the same thing. Deliberately permissive:
|
|
58
|
+
* a missed match costs a phantom add plus a phantom remove, which is two wrong
|
|
59
|
+
* lines, while a wrong match costs one wrong line. The renderer can survive a
|
|
60
|
+
* loose match; it cannot reconstruct a missed one.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MATCH_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element identity across two captures. The ladder from
|
|
3
|
+
* `docs/notes/01-identity-ladder.md`, made executable.
|
|
4
|
+
*
|
|
5
|
+
* Two hard rungs resolved before any scoring, then a scored match. The
|
|
6
|
+
* counter-intuitive part, and the one thing here most likely to be "fixed" back
|
|
7
|
+
* into a bug: **geometry is never a scoring feature.** See `geometryTiebreak`.
|
|
8
|
+
*/
|
|
9
|
+
import type { ElementRecord } from "../snapshot/types.js";
|
|
10
|
+
/** qain's set, verbatim. Roles too generic to identify anything. */
|
|
11
|
+
export declare const WEAK_ROLES: ReadonlySet<string>;
|
|
12
|
+
export declare function isGeneratedId(id: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Rungs 1 and 2: a key identifying the element wherever it sits.
|
|
15
|
+
*
|
|
16
|
+
* `null` when the element offers nothing document-unique and trustworthy, in
|
|
17
|
+
* which case it falls through to the scored rungs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hardKey(el: ElementRecord): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Levenshtein similarity in [0, 1], or `null` when neither side has the feature
|
|
22
|
+
* at all — which is different from both sides having it and disagreeing, and
|
|
23
|
+
* has to be, or two elements with no accessible name would be punished for it.
|
|
24
|
+
*
|
|
25
|
+
* Inputs are capped at 80 characters upstream, so O(n·m) is fine.
|
|
26
|
+
*/
|
|
27
|
+
export declare function textSimilarity(a: string | null | undefined, b: string | null | undefined): number | null;
|
|
28
|
+
export interface SimilarityInput {
|
|
29
|
+
ordinalA: number | undefined;
|
|
30
|
+
ordinalB: number | undefined;
|
|
31
|
+
subtreeTextA: string | undefined;
|
|
32
|
+
subtreeTextB: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rungs 3 and 4, as a normalised score in [0, 1].
|
|
36
|
+
*
|
|
37
|
+
* The denominator is the sum of weights of *applicable* features — those where
|
|
38
|
+
* at least one side has a value. Two bare divs therefore score 1.0 on tag and
|
|
39
|
+
* ordinal alone rather than being punished for having no role or name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function similarity(a: ElementRecord, b: ElementRecord, input: SimilarityInput): number;
|
|
42
|
+
/**
|
|
43
|
+
* A tiebreak, and only a tiebreak.
|
|
44
|
+
*
|
|
45
|
+
* Geometry is the payload being diffed. Score it and an element that moved far
|
|
46
|
+
* enough scores worse against itself than against some stationary neighbour, so
|
|
47
|
+
* the diff reports a removal plus an addition — erasing the single line the
|
|
48
|
+
* whole tool exists to print, and doing it *more* the bigger the layout change
|
|
49
|
+
* is. That is the wrong direction on every axis.
|
|
50
|
+
*
|
|
51
|
+
* Scaled to 1e-6 so it can only order candidates that already scored equal —
|
|
52
|
+
* three identical Delete buttons in a table row, where position is genuinely
|
|
53
|
+
* the only discriminator — and can never outvote role and name.
|
|
54
|
+
*/
|
|
55
|
+
export declare function geometryTiebreak(a: ElementRecord, b: ElementRecord): number;
|
|
56
|
+
/**
|
|
57
|
+
* Below this, two elements are not the same thing. Deliberately permissive:
|
|
58
|
+
* a missed match costs a phantom add plus a phantom remove, which is two wrong
|
|
59
|
+
* lines, while a wrong match costs one wrong line. The renderer can survive a
|
|
60
|
+
* loose match; it cannot reconstruct a missed one.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MATCH_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** qain's set, verbatim. Roles too generic to identify anything. */
|
|
2
|
+
export const WEAK_ROLES = new Set([
|
|
3
|
+
'generic',
|
|
4
|
+
'none',
|
|
5
|
+
'presentation',
|
|
6
|
+
'GenericContainer',
|
|
7
|
+
'InlineTextBox',
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* Ids that differ between two renders of the identical page.
|
|
11
|
+
*
|
|
12
|
+
* No source in the step-1 prior-art read has this guard: qain trusts `#id`
|
|
13
|
+
* unconditionally, and browser-use's dynamic filter covers classes only. React
|
|
14
|
+
* 18's `useId`, Radix, Headless UI, MUI, Emotion, styled-components and Angular
|
|
15
|
+
* all emit ids that change on every render, and treating one as a stable key
|
|
16
|
+
* makes an unchanged element look replaced.
|
|
17
|
+
*/
|
|
18
|
+
const GENERATED_ID = [
|
|
19
|
+
/:/, // React 18 useId -> ":r0:", ":R1mcq:"; Radix and Headless UI wrap it
|
|
20
|
+
/^mui-\d+$/,
|
|
21
|
+
/^ember\d+$/,
|
|
22
|
+
/^ng-tns-c\d+-\d+$/,
|
|
23
|
+
/^_ngcontent-\w+-c\d+$/,
|
|
24
|
+
/^css-[0-9a-z]{6,}$/i,
|
|
25
|
+
/^sc-[A-Za-z]{6,}$/,
|
|
26
|
+
/^[0-9a-f]{8,}$/i, // a bare hex or uuid run
|
|
27
|
+
];
|
|
28
|
+
export function isGeneratedId(id) {
|
|
29
|
+
return GENERATED_ID.some((pattern) => pattern.test(id));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Rungs 1 and 2: a key identifying the element wherever it sits.
|
|
33
|
+
*
|
|
34
|
+
* `null` when the element offers nothing document-unique and trustworthy, in
|
|
35
|
+
* which case it falls through to the scored rungs.
|
|
36
|
+
*/
|
|
37
|
+
export function hardKey(el) {
|
|
38
|
+
if (el.testId)
|
|
39
|
+
return `@${el.testId}`;
|
|
40
|
+
if (el.domId && !isGeneratedId(el.domId))
|
|
41
|
+
return `#${el.domId}`;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Feature weights.
|
|
46
|
+
*
|
|
47
|
+
* Similo's table puts location, area and shape in its lowest tier (0.5) even
|
|
48
|
+
* for its own task, which is finding an element on a page that is *supposed* to
|
|
49
|
+
* be unchanged. Here they are absent entirely; the rest is scaled around role
|
|
50
|
+
* and accessible name being the strongest available evidence of sameness.
|
|
51
|
+
*/
|
|
52
|
+
const W = {
|
|
53
|
+
role: 2.0,
|
|
54
|
+
name: 2.0,
|
|
55
|
+
tag: 1.5,
|
|
56
|
+
ownText: 1.0,
|
|
57
|
+
subtreeText: 1.0,
|
|
58
|
+
ordinal: 0.5,
|
|
59
|
+
};
|
|
60
|
+
const normalise = (s) => (s ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
61
|
+
/**
|
|
62
|
+
* Levenshtein similarity in [0, 1], or `null` when neither side has the feature
|
|
63
|
+
* at all — which is different from both sides having it and disagreeing, and
|
|
64
|
+
* has to be, or two elements with no accessible name would be punished for it.
|
|
65
|
+
*
|
|
66
|
+
* Inputs are capped at 80 characters upstream, so O(n·m) is fine.
|
|
67
|
+
*/
|
|
68
|
+
export function textSimilarity(a, b) {
|
|
69
|
+
const x = normalise(a);
|
|
70
|
+
const y = normalise(b);
|
|
71
|
+
if (!x && !y)
|
|
72
|
+
return null;
|
|
73
|
+
if (!x || !y)
|
|
74
|
+
return 0;
|
|
75
|
+
if (x === y)
|
|
76
|
+
return 1;
|
|
77
|
+
const m = x.length;
|
|
78
|
+
const n = y.length;
|
|
79
|
+
let previous = Array.from({ length: n + 1 }, (_, j) => j);
|
|
80
|
+
for (let i = 1; i <= m; i++) {
|
|
81
|
+
const current = [i];
|
|
82
|
+
for (let j = 1; j <= n; j++) {
|
|
83
|
+
const substitution = (previous[j - 1] ?? 0) + (x[i - 1] === y[j - 1] ? 0 : 1);
|
|
84
|
+
current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution);
|
|
85
|
+
}
|
|
86
|
+
previous = current;
|
|
87
|
+
}
|
|
88
|
+
return 1 - (previous[n] ?? 0) / Math.max(m, n);
|
|
89
|
+
}
|
|
90
|
+
const usableRole = (el) => el.role && !WEAK_ROLES.has(el.role) ? el.role : null;
|
|
91
|
+
/**
|
|
92
|
+
* Rungs 3 and 4, as a normalised score in [0, 1].
|
|
93
|
+
*
|
|
94
|
+
* The denominator is the sum of weights of *applicable* features — those where
|
|
95
|
+
* at least one side has a value. Two bare divs therefore score 1.0 on tag and
|
|
96
|
+
* ordinal alone rather than being punished for having no role or name.
|
|
97
|
+
*/
|
|
98
|
+
export function similarity(a, b, input) {
|
|
99
|
+
let got = 0;
|
|
100
|
+
let possible = 0;
|
|
101
|
+
const roleA = usableRole(a);
|
|
102
|
+
const roleB = usableRole(b);
|
|
103
|
+
if (roleA || roleB) {
|
|
104
|
+
possible += W.role;
|
|
105
|
+
if (roleA && roleA === roleB)
|
|
106
|
+
got += W.role;
|
|
107
|
+
}
|
|
108
|
+
const nameSimilarity = textSimilarity(a.name, b.name);
|
|
109
|
+
if (nameSimilarity !== null) {
|
|
110
|
+
possible += W.name;
|
|
111
|
+
got += W.name * nameSimilarity;
|
|
112
|
+
}
|
|
113
|
+
possible += W.tag;
|
|
114
|
+
if (a.tag === b.tag)
|
|
115
|
+
got += W.tag;
|
|
116
|
+
const ownSimilarity = textSimilarity(a.text, b.text);
|
|
117
|
+
if (ownSimilarity !== null) {
|
|
118
|
+
possible += W.ownText;
|
|
119
|
+
got += W.ownText * ownSimilarity;
|
|
120
|
+
}
|
|
121
|
+
// Subtree text is a weighted feature with a graded comparator, never part of
|
|
122
|
+
// a key, and it never propagates into a parent's identity. That is the whole
|
|
123
|
+
// difference between it and the text-hash approach the critique showed
|
|
124
|
+
// cascading: there, one child's text edit re-keyed its ancestors.
|
|
125
|
+
const subtreeSimilarity = textSimilarity(input.subtreeTextA, input.subtreeTextB);
|
|
126
|
+
if (subtreeSimilarity !== null) {
|
|
127
|
+
possible += W.subtreeText;
|
|
128
|
+
got += W.subtreeText * subtreeSimilarity;
|
|
129
|
+
}
|
|
130
|
+
possible += W.ordinal;
|
|
131
|
+
if (input.ordinalA === input.ordinalB)
|
|
132
|
+
got += W.ordinal;
|
|
133
|
+
return possible === 0 ? 0 : got / possible;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A tiebreak, and only a tiebreak.
|
|
137
|
+
*
|
|
138
|
+
* Geometry is the payload being diffed. Score it and an element that moved far
|
|
139
|
+
* enough scores worse against itself than against some stationary neighbour, so
|
|
140
|
+
* the diff reports a removal plus an addition — erasing the single line the
|
|
141
|
+
* whole tool exists to print, and doing it *more* the bigger the layout change
|
|
142
|
+
* is. That is the wrong direction on every axis.
|
|
143
|
+
*
|
|
144
|
+
* Scaled to 1e-6 so it can only order candidates that already scored equal —
|
|
145
|
+
* three identical Delete buttons in a table row, where position is genuinely
|
|
146
|
+
* the only discriminator — and can never outvote role and name.
|
|
147
|
+
*/
|
|
148
|
+
export function geometryTiebreak(a, b) {
|
|
149
|
+
const dx = a.rect.x - b.rect.x;
|
|
150
|
+
const dy = a.rect.y - b.rect.y;
|
|
151
|
+
return 1e-6 / (1 + Math.sqrt(dx * dx + dy * dy));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Below this, two elements are not the same thing. Deliberately permissive:
|
|
155
|
+
* a missed match costs a phantom add plus a phantom remove, which is two wrong
|
|
156
|
+
* lines, while a wrong match costs one wrong line. The renderer can survive a
|
|
157
|
+
* loose match; it cannot reconstruct a missed one.
|
|
158
|
+
*/
|
|
159
|
+
export const MATCH_THRESHOLD = 0.5;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiffViewportMismatchError = exports.renderDiff = exports.prepare = exports.match = exports.WEAK_ROLES = exports.textSimilarity = exports.similarity = exports.MATCH_THRESHOLD = exports.isGeneratedId = exports.hardKey = exports.geometryTiebreak = exports.computeChanges = void 0;
|
|
4
|
+
exports.diffCaptures = diffCaptures;
|
|
5
|
+
const changes_js_1 = require("./changes.cjs");
|
|
6
|
+
const match_js_1 = require("./match.cjs");
|
|
7
|
+
const render_js_1 = require("./render.cjs");
|
|
8
|
+
var changes_js_2 = require("./changes.cjs");
|
|
9
|
+
Object.defineProperty(exports, "computeChanges", { enumerable: true, get: function () { return changes_js_2.computeChanges; } });
|
|
10
|
+
var identity_js_1 = require("./identity.cjs");
|
|
11
|
+
Object.defineProperty(exports, "geometryTiebreak", { enumerable: true, get: function () { return identity_js_1.geometryTiebreak; } });
|
|
12
|
+
Object.defineProperty(exports, "hardKey", { enumerable: true, get: function () { return identity_js_1.hardKey; } });
|
|
13
|
+
Object.defineProperty(exports, "isGeneratedId", { enumerable: true, get: function () { return identity_js_1.isGeneratedId; } });
|
|
14
|
+
Object.defineProperty(exports, "MATCH_THRESHOLD", { enumerable: true, get: function () { return identity_js_1.MATCH_THRESHOLD; } });
|
|
15
|
+
Object.defineProperty(exports, "similarity", { enumerable: true, get: function () { return identity_js_1.similarity; } });
|
|
16
|
+
Object.defineProperty(exports, "textSimilarity", { enumerable: true, get: function () { return identity_js_1.textSimilarity; } });
|
|
17
|
+
Object.defineProperty(exports, "WEAK_ROLES", { enumerable: true, get: function () { return identity_js_1.WEAK_ROLES; } });
|
|
18
|
+
var match_js_2 = require("./match.cjs");
|
|
19
|
+
Object.defineProperty(exports, "match", { enumerable: true, get: function () { return match_js_2.match; } });
|
|
20
|
+
Object.defineProperty(exports, "prepare", { enumerable: true, get: function () { return match_js_2.prepare; } });
|
|
21
|
+
var render_js_2 = require("./render.cjs");
|
|
22
|
+
Object.defineProperty(exports, "renderDiff", { enumerable: true, get: function () { return render_js_2.renderDiff; } });
|
|
23
|
+
/**
|
|
24
|
+
* Two captures were taken at different viewports.
|
|
25
|
+
*
|
|
26
|
+
* Nearly every box moves on a viewport change, so a diff would faithfully
|
|
27
|
+
* report hundreds of consequences of something the reader already knows
|
|
28
|
+
* instead of the change they are looking for. `cli.ts` has refused this for
|
|
29
|
+
* as long as the `diff` verb has existed; `diffCaptures` itself did not,
|
|
30
|
+
* which meant any other caller of the library — not just the CLI — could
|
|
31
|
+
* diff 1440×900 against 375×812 and get a diff that describes a resize.
|
|
32
|
+
*/
|
|
33
|
+
class DiffViewportMismatchError extends Error {
|
|
34
|
+
before;
|
|
35
|
+
after;
|
|
36
|
+
name = 'DiffViewportMismatchError';
|
|
37
|
+
constructor(before, after) {
|
|
38
|
+
super(`dnumb: these captures were taken at different viewports (${before.width}x${before.height} ` +
|
|
39
|
+
`vs ${after.width}x${after.height}). Nearly every box moved, so the diff would describe ` +
|
|
40
|
+
'the resize rather than the change you are looking for. Capture both states at one ' +
|
|
41
|
+
'viewport, or read each with `look`.');
|
|
42
|
+
this.before = before;
|
|
43
|
+
this.after = after;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.DiffViewportMismatchError = DiffViewportMismatchError;
|
|
47
|
+
/**
|
|
48
|
+
* Diff two captures end to end.
|
|
49
|
+
*
|
|
50
|
+
* Returns both the rendered text and the structured changes, so a caller that
|
|
51
|
+
* wants numbers does not have to parse the text back — the same discipline the
|
|
52
|
+
* query verbs follow, and for the same reason: two renderings built from one
|
|
53
|
+
* computation cannot disagree.
|
|
54
|
+
*/
|
|
55
|
+
function diffCaptures(before, after, options = {}) {
|
|
56
|
+
if (before.viewport.width !== after.viewport.width ||
|
|
57
|
+
before.viewport.height !== after.viewport.height) {
|
|
58
|
+
throw new DiffViewportMismatchError(before.viewport, after.viewport);
|
|
59
|
+
}
|
|
60
|
+
const preparedBefore = (0, match_js_1.prepare)(before);
|
|
61
|
+
const preparedAfter = (0, match_js_1.prepare)(after);
|
|
62
|
+
const matched = (0, match_js_1.match)(preparedBefore, preparedAfter);
|
|
63
|
+
const changes = (0, changes_js_1.computeChanges)(preparedBefore, preparedAfter, matched);
|
|
64
|
+
const viewport = (snapshot) => `${snapshot.viewport.width}x${snapshot.viewport.height}`;
|
|
65
|
+
const text = (0, render_js_1.renderDiff)(changes, {
|
|
66
|
+
before: options.beforeName ?? 'before',
|
|
67
|
+
after: options.afterName ?? 'after',
|
|
68
|
+
viewportBefore: viewport(before),
|
|
69
|
+
viewportAfter: viewport(after),
|
|
70
|
+
truncatedBefore: before.truncated,
|
|
71
|
+
truncatedAfter: after.truncated,
|
|
72
|
+
beforeCount: before.elements.length,
|
|
73
|
+
afterCount: after.elements.length,
|
|
74
|
+
// `exactOptionalPropertyTypes` treats `key: undefined` as distinct from
|
|
75
|
+
// the key being absent, so an `undefined` strategy/notMeasured (a
|
|
76
|
+
// hand-built `Snapshot` fixture with no opinion) has to be omitted
|
|
77
|
+
// entirely, not assigned.
|
|
78
|
+
...(before.strategy !== undefined ? { strategyBefore: before.strategy } : {}),
|
|
79
|
+
...(after.strategy !== undefined ? { strategyAfter: after.strategy } : {}),
|
|
80
|
+
...(before.notMeasured !== undefined ? { notMeasuredBefore: before.notMeasured } : {}),
|
|
81
|
+
...(after.notMeasured !== undefined ? { notMeasuredAfter: after.notMeasured } : {}),
|
|
82
|
+
}, options.maxLines === undefined ? {} : { maxLines: options.maxLines });
|
|
83
|
+
return { changes, text, truncated: { before: before.truncated, after: after.truncated } };
|
|
84
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state diff: what changed between two captures, and why.
|
|
3
|
+
*
|
|
4
|
+
* `look` describes one moment. This describes the transition — which is what
|
|
5
|
+
* makes an agent's own actions legible to it. Click something, get back a
|
|
6
|
+
* bounded account of what the click did, instead of re-reading the whole digest
|
|
7
|
+
* and comparing by hand.
|
|
8
|
+
*
|
|
9
|
+
* The hard part is not the arithmetic. Before anything can say `e12 moved down
|
|
10
|
+
* 21px`, it has to know that `e12` in one capture and `e12` in the other are
|
|
11
|
+
* the same element across a DOM that just changed. `identity.ts` is that, and
|
|
12
|
+
* its central finding is that geometry must count for *zero* when matching.
|
|
13
|
+
*/
|
|
14
|
+
import type { Snapshot } from "../snapshot/types.cjs";
|
|
15
|
+
import type { DiffOptions, DiffResult } from "./types.cjs";
|
|
16
|
+
export { computeChanges } from "./changes.cjs";
|
|
17
|
+
export { geometryTiebreak, hardKey, isGeneratedId, MATCH_THRESHOLD, similarity, textSimilarity, WEAK_ROLES, } from "./identity.cjs";
|
|
18
|
+
export { match, prepare } from "./match.cjs";
|
|
19
|
+
export { renderDiff } from "./render.cjs";
|
|
20
|
+
export type { BoxDelta, Cause, Change, ChangeKind, Derivation, DiffMeta, DiffOptions, DiffResult, Match, Prepared, Subsumption, } from "./types.cjs";
|
|
21
|
+
export interface DiffCapturesOptions extends DiffOptions {
|
|
22
|
+
/** Names for the two captures, used in the header. Default `before`/`after`. */
|
|
23
|
+
beforeName?: string;
|
|
24
|
+
afterName?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What `diffCaptures` actually needs beyond a bare `Snapshot`: which capture
|
|
28
|
+
* path produced it, when the caller has one to give.
|
|
29
|
+
*
|
|
30
|
+
* `strategy` lives on `CaptureResult` and `Bundle`, not on `Snapshot` itself —
|
|
31
|
+
* this package's diff layer only depends on `snapshot/types.ts`. Both real
|
|
32
|
+
* capture shapes are structural supersets of `Snapshot`, so a caller passing
|
|
33
|
+
* a real `CaptureResult` or `Bundle` satisfies this type for free; only a
|
|
34
|
+
* hand-built `Snapshot` fixture has no opinion, which is why the field is
|
|
35
|
+
* optional here exactly as `DiffMeta.strategyBefore` is.
|
|
36
|
+
*/
|
|
37
|
+
export interface DiffableSnapshot extends Snapshot {
|
|
38
|
+
strategy?: 'cdp' | 'walk';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Two captures were taken at different viewports.
|
|
42
|
+
*
|
|
43
|
+
* Nearly every box moves on a viewport change, so a diff would faithfully
|
|
44
|
+
* report hundreds of consequences of something the reader already knows
|
|
45
|
+
* instead of the change they are looking for. `cli.ts` has refused this for
|
|
46
|
+
* as long as the `diff` verb has existed; `diffCaptures` itself did not,
|
|
47
|
+
* which meant any other caller of the library — not just the CLI — could
|
|
48
|
+
* diff 1440×900 against 375×812 and get a diff that describes a resize.
|
|
49
|
+
*/
|
|
50
|
+
export declare class DiffViewportMismatchError extends Error {
|
|
51
|
+
readonly before: {
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
};
|
|
55
|
+
readonly after: {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
};
|
|
59
|
+
name: string;
|
|
60
|
+
constructor(before: {
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
}, after: {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Diff two captures end to end.
|
|
70
|
+
*
|
|
71
|
+
* Returns both the rendered text and the structured changes, so a caller that
|
|
72
|
+
* wants numbers does not have to parse the text back — the same discipline the
|
|
73
|
+
* query verbs follow, and for the same reason: two renderings built from one
|
|
74
|
+
* computation cannot disagree.
|
|
75
|
+
*/
|
|
76
|
+
export declare function diffCaptures(before: DiffableSnapshot, after: DiffableSnapshot, options?: DiffCapturesOptions): DiffResult;
|