@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,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;