@juxtapose/core 0.4.0 → 0.5.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 (131) hide show
  1. package/dist/agent/handoff.d.ts +8 -1
  2. package/dist/agent/handoff.js +75 -7
  3. package/dist/agent/handoff.js.map +1 -1
  4. package/dist/analysis/affected.d.ts +37 -0
  5. package/dist/analysis/affected.js +97 -22
  6. package/dist/analysis/affected.js.map +1 -1
  7. package/dist/analysis/cause.d.ts +32 -0
  8. package/dist/analysis/cause.js +187 -0
  9. package/dist/analysis/cause.js.map +1 -0
  10. package/dist/baseline/approve.d.ts +24 -0
  11. package/dist/baseline/approve.js +54 -0
  12. package/dist/baseline/approve.js.map +1 -0
  13. package/dist/baseline/git-store.d.ts +4 -0
  14. package/dist/baseline/git-store.js +22 -0
  15. package/dist/baseline/git-store.js.map +1 -1
  16. package/dist/browser/acquire.d.ts +11 -0
  17. package/dist/browser/acquire.js +15 -0
  18. package/dist/browser/acquire.js.map +1 -0
  19. package/dist/browser/browser.d.ts +53 -0
  20. package/dist/browser/browser.js +122 -0
  21. package/dist/browser/browser.js.map +1 -0
  22. package/dist/browser/cdp.d.ts +81 -0
  23. package/dist/browser/cdp.js +230 -0
  24. package/dist/browser/cdp.js.map +1 -0
  25. package/dist/browser/index.d.ts +9 -0
  26. package/dist/browser/index.js +10 -0
  27. package/dist/browser/index.js.map +1 -0
  28. package/dist/browser/install.d.ts +33 -0
  29. package/dist/browser/install.js +167 -0
  30. package/dist/browser/install.js.map +1 -0
  31. package/dist/browser/launch.d.ts +37 -0
  32. package/dist/browser/launch.js +265 -0
  33. package/dist/browser/launch.js.map +1 -0
  34. package/dist/browser/page.d.ts +125 -0
  35. package/dist/browser/page.js +564 -0
  36. package/dist/browser/page.js.map +1 -0
  37. package/dist/browser/pins.d.ts +12 -0
  38. package/dist/browser/pins.js +57 -0
  39. package/dist/browser/pins.js.map +1 -0
  40. package/dist/browser/resolve.d.ts +25 -0
  41. package/dist/browser/resolve.js +115 -0
  42. package/dist/browser/resolve.js.map +1 -0
  43. package/dist/browser/selectors.d.ts +10 -0
  44. package/dist/browser/selectors.js +60 -0
  45. package/dist/browser/selectors.js.map +1 -0
  46. package/dist/browser/transport.d.ts +39 -0
  47. package/dist/browser/transport.js +99 -0
  48. package/dist/browser/transport.js.map +1 -0
  49. package/dist/browser/zip.d.ts +27 -0
  50. package/dist/browser/zip.js +241 -0
  51. package/dist/browser/zip.js.map +1 -0
  52. package/dist/capture/capture.d.ts +7 -1
  53. package/dist/capture/capture.js +25 -8
  54. package/dist/capture/capture.js.map +1 -1
  55. package/dist/capture/env.d.ts +1 -1
  56. package/dist/capture/launch.d.ts +19 -1
  57. package/dist/capture/launch.js +91 -1
  58. package/dist/capture/launch.js.map +1 -1
  59. package/dist/capture/pool.d.ts +1 -1
  60. package/dist/capture/pool.js +41 -8
  61. package/dist/capture/pool.js.map +1 -1
  62. package/dist/capture/settle.d.ts +1 -1
  63. package/dist/capture/settle.js +2 -0
  64. package/dist/capture/settle.js.map +1 -1
  65. package/dist/capture/snap.d.ts +43 -0
  66. package/dist/capture/snap.js +103 -0
  67. package/dist/capture/snap.js.map +1 -0
  68. package/dist/capture/structure.d.ts +77 -0
  69. package/dist/capture/structure.js +498 -0
  70. package/dist/capture/structure.js.map +1 -0
  71. package/dist/diff/compare.d.ts +19 -0
  72. package/dist/diff/compare.js +19 -0
  73. package/dist/diff/compare.js.map +1 -0
  74. package/dist/diff/crops.d.ts +26 -0
  75. package/dist/diff/crops.js +92 -0
  76. package/dist/diff/crops.js.map +1 -0
  77. package/dist/diff/structural/classify.d.ts +29 -0
  78. package/dist/diff/structural/classify.js +113 -0
  79. package/dist/diff/structural/classify.js.map +1 -0
  80. package/dist/diff/structural/coverage.d.ts +11 -0
  81. package/dist/diff/structural/coverage.js +37 -0
  82. package/dist/diff/structural/coverage.js.map +1 -0
  83. package/dist/diff/structural/deltas.d.ts +29 -0
  84. package/dist/diff/structural/deltas.js +218 -0
  85. package/dist/diff/structural/deltas.js.map +1 -0
  86. package/dist/diff/structural/disambiguate.d.ts +3 -0
  87. package/dist/diff/structural/disambiguate.js +89 -0
  88. package/dist/diff/structural/disambiguate.js.map +1 -0
  89. package/dist/diff/structural/label.d.ts +15 -0
  90. package/dist/diff/structural/label.js +64 -0
  91. package/dist/diff/structural/label.js.map +1 -0
  92. package/dist/diff/structural/match.d.ts +13 -0
  93. package/dist/diff/structural/match.js +135 -0
  94. package/dist/diff/structural/match.js.map +1 -0
  95. package/dist/diff/structural/shift.d.ts +2 -0
  96. package/dist/diff/structural/shift.js +144 -0
  97. package/dist/diff/structural/shift.js.map +1 -0
  98. package/dist/diff/structural/summary.d.ts +31 -0
  99. package/dist/diff/structural/summary.js +420 -0
  100. package/dist/diff/structural/summary.js.map +1 -0
  101. package/dist/diff/structural.d.ts +40 -0
  102. package/dist/diff/structural.js +219 -0
  103. package/dist/diff/structural.js.map +1 -0
  104. package/dist/index.d.ts +25 -3
  105. package/dist/index.js +29 -2
  106. package/dist/index.js.map +1 -1
  107. package/dist/platform.d.ts +1 -1
  108. package/dist/platforms/urls.d.ts +3 -0
  109. package/dist/platforms/urls.js +89 -0
  110. package/dist/platforms/urls.js.map +1 -0
  111. package/dist/report/comment.js +9 -3
  112. package/dist/report/comment.js.map +1 -1
  113. package/dist/report/format.d.ts +18 -0
  114. package/dist/report/format.js +20 -0
  115. package/dist/report/format.js.map +1 -1
  116. package/dist/report/html.js +2 -2
  117. package/dist/report/html.js.map +1 -1
  118. package/dist/run/groups.d.ts +2 -0
  119. package/dist/run/groups.js +45 -0
  120. package/dist/run/groups.js.map +1 -0
  121. package/dist/run/run.d.ts +6 -2
  122. package/dist/run/run.js +74 -6
  123. package/dist/run/run.js.map +1 -1
  124. package/dist/shell-quote.d.ts +2 -0
  125. package/dist/shell-quote.js +10 -0
  126. package/dist/shell-quote.js.map +1 -0
  127. package/dist/types.d.ts +199 -1
  128. package/dist/url-platform.d.ts +2 -2
  129. package/dist/url-platform.js +2 -2
  130. package/dist/url-platform.js.map +1 -1
  131. package/package.json +5 -10
@@ -0,0 +1,89 @@
1
+ // A label is a description, not an identity: a page with two cards has two
2
+ // `div.card`s, and when both change, every reader of the verdict — the JSON,
3
+ // the summary, `describeDeltas` — sees one element. Measured on a dark-mode
4
+ // probe: the two cards' facts merged into one clause that stated
5
+ // `background-color` twice; on a 12-item reorder, seven `span.meta` moves
6
+ // read as one line and six deltas vanished from the human output.
7
+ //
8
+ // So within one verdict, distinct elements get distinct labels. Only labels
9
+ // that actually collide change, and they take the shortest stable
10
+ // distinguisher: the nearest ancestor with a name, id or test id
11
+ // (`span.meta in listitem[name="Item 4"]`), or, when that does not separate
12
+ // them, the element's ordinal among same-labelled elements on its side of the
13
+ // diff (`div.card (2nd)`). Both are properties of the page, not of the diff,
14
+ // so the same change labels the same way on every run.
15
+ import { MAX_LABEL, clip, label } from './label.js';
16
+ export function disambiguate(facts, baseline, current) {
17
+ const snap = (s) => (s.side === 'baseline' ? baseline : current);
18
+ const nth = {
19
+ baseline: undefined,
20
+ current: undefined,
21
+ };
22
+ const nthOf = (s) => (nth[s.side] ??= ordinals(snap(s)))[s.index];
23
+ const key = (s) => `${s.side}:${s.index}`;
24
+ const labelOf = (f) => ('el' in f.delta ? f.delta.el : f.delta.after);
25
+ // label → the distinct elements wearing it, in first-seen (fact) order
26
+ const byLabel = new Map();
27
+ for (const f of facts) {
28
+ const l = labelOf(f);
29
+ let ids = byLabel.get(l);
30
+ if (!ids)
31
+ byLabel.set(l, (ids = new Map()));
32
+ ids.set(key(f.source), f.source);
33
+ }
34
+ const rename = new Map();
35
+ for (const [l, ids] of byLabel) {
36
+ if (ids.size < 2)
37
+ continue;
38
+ const sources = [...ids.values()];
39
+ const scoped = sources.map((s) => {
40
+ const a = labelledAncestor(snap(s), s.index);
41
+ return a === undefined ? undefined : clip(`${l} in ${a}`, MAX_LABEL);
42
+ });
43
+ const distinct = scoped.every((x) => x !== undefined) && new Set(scoped).size === scoped.length;
44
+ for (const [k, s] of sources.entries()) {
45
+ const suffix = ` (${ordinal(nthOf(s))})`;
46
+ rename.set(key(s), distinct ? scoped[k] : clip(l, MAX_LABEL - suffix.length) + suffix);
47
+ }
48
+ }
49
+ if (rename.size === 0)
50
+ return facts;
51
+ return facts.map((f) => {
52
+ const to = rename.get(key(f.source));
53
+ if (to === undefined)
54
+ return f;
55
+ const d = f.delta;
56
+ return { ...f, delta: 'el' in d ? { ...d, el: to } : { ...d, after: to } };
57
+ });
58
+ }
59
+ /** Label of the nearest ancestor that names itself (role+name, id, test id);
60
+ * a class or a path does not tell two siblings' parents apart. */
61
+ function labelledAncestor(s, index) {
62
+ for (let p = s.elements[index].parent; p !== null; p = s.elements[p].parent) {
63
+ const e = s.elements[p];
64
+ if (e.tag === 'masked')
65
+ continue;
66
+ if ((e.role && e.name) || e.id !== undefined || e.testId !== undefined)
67
+ return label(e);
68
+ }
69
+ return undefined;
70
+ }
71
+ /** Each element's 1-based position among its side's elements that carry the
72
+ * same label, in document order. One pass per side, not one per element: a
73
+ * list of 5,000 identical rows that all changed would otherwise be 25M
74
+ * label calls. */
75
+ function ordinals(s) {
76
+ const seen = new Map();
77
+ return s.elements.map((e) => {
78
+ const l = label(e);
79
+ const n = (seen.get(l) ?? 0) + 1;
80
+ seen.set(l, n);
81
+ return n;
82
+ });
83
+ }
84
+ function ordinal(n) {
85
+ const teen = n % 100 >= 11 && n % 100 <= 13;
86
+ const suffix = teen ? 'th' : (['th', 'st', 'nd', 'rd'][n % 10] ?? 'th');
87
+ return `${n}${suffix}`;
88
+ }
89
+ //# sourceMappingURL=disambiguate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disambiguate.js","sourceRoot":"","sources":["../../../src/diff/structural/disambiguate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { StructureElement } from '../../types.js';
2
+ /** Elements that paint their own pixels: a change inside them leaves the DOM
3
+ * and computed styles untouched, so unexplained pixels on one of these mean
4
+ * "content", not "drift". */
5
+ export declare const SELF_PAINTING: ReadonlySet<string>;
6
+ /** Longest label in a delta or summary. A deep path or a generated id or
7
+ * class must not be able to spend the verdict's 1,500-char budget. */
8
+ export declare const MAX_LABEL = 60;
9
+ export declare function label(el: StructureElement): string;
10
+ /** At most `max` UTF-16 units, ending in `…` when cut, never splitting a
11
+ * surrogate pair (half an emoji is invalid UTF-16 in the JSON). */
12
+ export declare function clip(s: string, max: number): string;
13
+ /** Identity with the page-specific part removed, so the same change on two
14
+ * pages hashes to the same signature. */
15
+ export declare function abstractLabel(el: StructureElement): string;
@@ -0,0 +1,64 @@
1
+ // Short human labels for elements in deltas and summaries. The budget is
2
+ // ~1,500 chars for a whole verdict, so a label is the shortest thing that
3
+ // still tells an agent which element to look at.
4
+ /** Elements that paint their own pixels: a change inside them leaves the DOM
5
+ * and computed styles untouched, so unexplained pixels on one of these mean
6
+ * "content", not "drift". */
7
+ export const SELF_PAINTING = new Set([
8
+ 'canvas',
9
+ 'video',
10
+ 'img',
11
+ 'iframe',
12
+ 'object',
13
+ 'embed',
14
+ ]);
15
+ /** Longest label in a delta or summary. A deep path or a generated id or
16
+ * class must not be able to spend the verdict's 1,500-char budget. */
17
+ export const MAX_LABEL = 60;
18
+ export function label(el) {
19
+ if (el.tag === 'masked')
20
+ return 'masked';
21
+ if (el.role && el.name)
22
+ return clip(`${el.role}[name="${clip(el.name, 40)}"]`, MAX_LABEL);
23
+ if (el.id !== undefined)
24
+ return clip(`${el.tag}#${el.id}`, MAX_LABEL);
25
+ if (el.testId !== undefined)
26
+ return clip(`${el.tag}[data-testid="${el.testId}"]`, MAX_LABEL);
27
+ if (el.classes?.length)
28
+ return clip(`${el.tag}.${el.classes[0]}`, MAX_LABEL);
29
+ return clipPath(el.path
30
+ .replace(/^html:nth-of-type\(1\)>body:nth-of-type\(1\)>/, '')
31
+ .replace(/:nth-of-type\(1\)/g, ''));
32
+ }
33
+ /** A path keeps its tail — the element itself and its nearest ancestors —
34
+ * cut at a `>` boundary, so what remains is still a valid selector suffix. */
35
+ function clipPath(path) {
36
+ if (path.length <= MAX_LABEL)
37
+ return path;
38
+ const parts = path.split('>');
39
+ let out = parts[parts.length - 1];
40
+ for (let k = parts.length - 2; k >= 0; k--) {
41
+ const next = `${parts[k]}>${out}`;
42
+ if (next.length + 2 > MAX_LABEL)
43
+ break;
44
+ out = next;
45
+ }
46
+ return `…>${clip(out, MAX_LABEL - 2)}`;
47
+ }
48
+ /** At most `max` UTF-16 units, ending in `…` when cut, never splitting a
49
+ * surrogate pair (half an emoji is invalid UTF-16 in the JSON). */
50
+ export function clip(s, max) {
51
+ if (s.length <= max)
52
+ return s;
53
+ let end = max - 1;
54
+ const c = s.charCodeAt(end - 1);
55
+ if (c >= 0xd800 && c <= 0xdbff)
56
+ end--;
57
+ return s.slice(0, end) + '…';
58
+ }
59
+ /** Identity with the page-specific part removed, so the same change on two
60
+ * pages hashes to the same signature. */
61
+ export function abstractLabel(el) {
62
+ return el.role ?? el.tag;
63
+ }
64
+ //# sourceMappingURL=label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label.js","sourceRoot":"","sources":["../../../src/diff/structural/label.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,0EAA0E;AAC1E,iDAAiD;AAIjD;;8BAE8B;AAC9B,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC;IACxD,QAAQ;IACR,OAAO;IACP,KAAK;IACL,QAAQ;IACR,QAAQ;IACR,OAAO;CACR,CAAC,CAAC;AAEH;uEACuE;AACvE,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAE5B,MAAM,UAAU,KAAK,CAAC,EAAoB;IACxC,IAAI,EAAE,CAAC,GAAG,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IACzC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI;QACpB,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACpE,IAAI,EAAE,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;IACtE,IAAI,EAAE,CAAC,MAAM,KAAK,SAAS;QACzB,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,iBAAiB,EAAE,CAAC,MAAM,IAAI,EAAE,SAAS,CAAC,CAAC;IAClE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC7E,OAAO,QAAQ,CACb,EAAE,CAAC,IAAI;SACJ,OAAO,CAAC,+CAA+C,EAAE,EAAE,CAAC;SAC5D,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CACrC,CAAC;AACJ,CAAC;AAED;+EAC+E;AAC/E,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAClC,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS;YAAE,MAAM;QACvC,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,OAAO,KAAK,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC;AACzC,CAAC;AAED;oEACoE;AACpE,MAAM,UAAU,IAAI,CAAC,CAAS,EAAE,GAAW;IACzC,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM;QAAE,GAAG,EAAE,CAAC;IACtC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AAC/B,CAAC;AAED;0CAC0C;AAC1C,MAAM,UAAU,aAAa,CAAC,EAAoB;IAChD,OAAO,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { StructureElement, StructureSnapshot } from '../../types.js';
2
+ export interface ElementMatch {
3
+ /** [baseline index, current index], sorted by current index. */
4
+ pairs: Array<[number, number]>;
5
+ /** Current indices of unmatched subtree roots, ascending. */
6
+ added: number[];
7
+ /** Baseline indices of unmatched subtree roots, ascending. */
8
+ removed: number[];
9
+ }
10
+ /** What makes an element "the same one" across captures: tag plus the most
11
+ * stable identity it carries. Masked stand-ins have only their tag. */
12
+ export declare function identityKey(el: StructureElement): string;
13
+ export declare function matchElements(a: StructureSnapshot, b: StructureSnapshot): ElementMatch;
@@ -0,0 +1,135 @@
1
+ // Which element on the baseline side is which element on the current side.
2
+ //
3
+ // Positional paths alone would report every sibling after an insertion as
4
+ // changed. Instead children are aligned per parent with an LCS over an
5
+ // identity key, recursing into matches, so an insertion is one `added` and the
6
+ // rest stay matched. LCS is per parent, so cost is Σ(children²) — small for
7
+ // real DOMs; a pathological flat parent falls back to in-order key pairing.
8
+ /** Above this many DP cells for one parent, pair greedily instead. */
9
+ const LCS_CELL_CAP = 250_000;
10
+ /** What makes an element "the same one" across captures: tag plus the most
11
+ * stable identity it carries. Masked stand-ins have only their tag. */
12
+ export function identityKey(el) {
13
+ if (el.id !== undefined)
14
+ return `${el.tag}#${el.id}`;
15
+ if (el.testId !== undefined)
16
+ return `${el.tag}[${el.testId}]`;
17
+ if (el.role && el.name)
18
+ return `${el.tag}|${el.role}=${el.name}`;
19
+ if (el.classes?.length)
20
+ return `${el.tag}.${el.classes.join('.')}`;
21
+ return el.tag;
22
+ }
23
+ export function matchElements(a, b) {
24
+ const childrenA = childLists(a.elements);
25
+ const childrenB = childLists(b.elements);
26
+ const keyA = a.elements.map(identityKey);
27
+ const keyB = b.elements.map(identityKey);
28
+ const pairs = [];
29
+ const added = [];
30
+ const removed = [];
31
+ const walk = (pa, pb) => {
32
+ const ca = childrenA.get(pa) ?? [];
33
+ const cb = childrenB.get(pb) ?? [];
34
+ const first = align(ca, cb, (i, j) => keyA[i] === keyB[j]);
35
+ // A text edit changes a name-derived key, a toggled class changes a
36
+ // class-derived one. Pair what is left by tag, in order, so those read as
37
+ // `text`/`attr` deltas rather than removed + added.
38
+ // Never across conflicting explicit identities: `div#row-1` removed and
39
+ // `div#banner` added are two different elements, not one edited one.
40
+ const second = align(first.leftA, first.leftB, (i, j) => sameKind(a.elements[i], b.elements[j]));
41
+ for (const [i, j] of [...first.matched, ...second.matched]) {
42
+ pairs.push([i, j]);
43
+ walk(i, j);
44
+ }
45
+ removed.push(...second.leftA);
46
+ added.push(...second.leftB);
47
+ };
48
+ walk(-1, -1);
49
+ pairs.sort((p, q) => p[1] - q[1]);
50
+ added.sort((x, y) => x - y);
51
+ removed.sort((x, y) => x - y);
52
+ return { pairs, added, removed };
53
+ }
54
+ /** Tag equal, and no explicit id or testId present on both sides and unequal. */
55
+ function sameKind(x, y) {
56
+ if (x.tag !== y.tag)
57
+ return false;
58
+ if (x.id !== undefined && y.id !== undefined && x.id !== y.id)
59
+ return false;
60
+ if (x.testId !== undefined && y.testId !== undefined && x.testId !== y.testId)
61
+ return false;
62
+ return true;
63
+ }
64
+ /** Children in document order, keyed by parent index; -1 is the root. */
65
+ function childLists(elements) {
66
+ const m = new Map();
67
+ elements.forEach((e, i) => {
68
+ const p = e.parent ?? -1;
69
+ const list = m.get(p);
70
+ if (list)
71
+ list.push(i);
72
+ else
73
+ m.set(p, [i]);
74
+ });
75
+ return m;
76
+ }
77
+ /** Order-preserving alignment of two sibling lists under `same`. */
78
+ function align(ca, cb, same) {
79
+ const matched = ca.length * cb.length > LCS_CELL_CAP ? greedy(ca, cb, same) : lcs(ca, cb, same);
80
+ const usedA = new Set(matched.map(([i]) => i));
81
+ const usedB = new Set(matched.map(([, j]) => j));
82
+ return {
83
+ matched,
84
+ leftA: ca.filter((i) => !usedA.has(i)),
85
+ leftB: cb.filter((j) => !usedB.has(j)),
86
+ };
87
+ }
88
+ function lcs(ca, cb, same) {
89
+ const n = ca.length;
90
+ const m = cb.length;
91
+ // L[i][j] = LCS length of ca[i..] and cb[j..]
92
+ const L = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
93
+ for (let i = n - 1; i >= 0; i--) {
94
+ for (let j = m - 1; j >= 0; j--) {
95
+ L[i][j] = same(ca[i], cb[j])
96
+ ? L[i + 1][j + 1] + 1
97
+ : Math.max(L[i + 1][j], L[i][j + 1]);
98
+ }
99
+ }
100
+ const out = [];
101
+ let i = 0;
102
+ let j = 0;
103
+ while (i < n && j < m) {
104
+ if (same(ca[i], cb[j])) {
105
+ out.push([ca[i], cb[j]]);
106
+ i++;
107
+ j++;
108
+ }
109
+ else if (L[i + 1][j] >= L[i][j + 1])
110
+ i++;
111
+ else
112
+ j++;
113
+ }
114
+ return out;
115
+ }
116
+ /** In-order pairing: each a takes the first unused b after the last pairing
117
+ * that is `same`. Linear-ish, order-preserving, and good enough for a parent
118
+ * with thousands of children where an exact LCS would cost seconds.
119
+ * Failure mode: it never looks backwards, so a reordered list degrades to
120
+ * many removed/added pairs instead of a minimal edit. */
121
+ function greedy(ca, cb, same) {
122
+ const out = [];
123
+ let j = 0;
124
+ for (const i of ca) {
125
+ let k = j;
126
+ while (k < cb.length && !same(i, cb[k]))
127
+ k++;
128
+ if (k < cb.length) {
129
+ out.push([i, cb[k]]);
130
+ j = k + 1;
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.js","sourceRoot":"","sources":["../../../src/diff/structural/match.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { type Fact } from './deltas.js';
2
+ export declare function synthesizeShifts(facts: Fact[]): Fact[];
@@ -0,0 +1,144 @@
1
+ // "Everything below the banner moved down 14px because the banner grew 14px"
2
+ // is one fact, not forty. Collapse a run of ≥3 matched elements that moved by
3
+ // the same (dx, dy), contiguous in document order, into one `shift` anchored
4
+ // on the element that explains it (see `collapse`).
5
+ //
6
+ // The anchor set is wider than the spec's added/removed/resized: a margin or
7
+ // line-height change moves everything after it without resizing anything the
8
+ // LCS can see, and leaving those as 40 `moved` deltas defeats the purpose.
9
+ import { compareCodeUnits, sortFacts } from './deltas.js';
10
+ const MIN_RUN = 3;
11
+ export function synthesizeShifts(facts) {
12
+ const anchors = facts.filter((f) => f.delta.kind !== 'moved' && f.delta.kind !== 'shift');
13
+ const moved = facts.filter((f) => f.delta.kind === 'moved');
14
+ if (moved.length < MIN_RUN)
15
+ return facts;
16
+ // Group by displacement, keeping document order within a group.
17
+ const groups = new Map();
18
+ for (const f of moved) {
19
+ const d = f.delta;
20
+ const key = `${d.dx},${d.dy}`;
21
+ const g = groups.get(key);
22
+ if (g)
23
+ g.push(f);
24
+ else
25
+ groups.set(key, [f]);
26
+ }
27
+ // A fact's position among all moves, in document order: two members of a
28
+ // group are adjacent exactly when no move with another displacement sits
29
+ // between them (unmoved elements in between — sticky headers, absolutes —
30
+ // do not break a run).
31
+ const pos = new Map(moved.map((f, k) => [f, k]));
32
+ const anchorOrders = anchors.map((f) => f.order); // ascending: facts are sorted
33
+ const collapsed = new Set();
34
+ const shifts = [];
35
+ // Deterministic: iterate groups by key, not by insertion.
36
+ for (const key of [...groups.keys()].sort(compareCodeUnits)) {
37
+ const g = groups.get(key);
38
+ // A different move inside a group splits it rather than vetoing it: in
39
+ // `font-size` the logo slides right between the button and the rows, and
40
+ // the six elements below still moved up together. So does another cause
41
+ // between two members: rows below a second removal moved because of that
42
+ // removal, not the first, so "480 elements after the first" would blame
43
+ // the wrong element (measured on the 50-section perf tree).
44
+ let start = 0;
45
+ for (let k = 1; k <= g.length; k++) {
46
+ if (k < g.length &&
47
+ pos.get(g[k]) === pos.get(g[k - 1]) + 1 &&
48
+ !causeBetween(anchorOrders, g[k - 1].order, g[k].order))
49
+ continue;
50
+ const run = g.slice(start, k);
51
+ start = k;
52
+ const shift = run.length >= MIN_RUN ? collapse(run, anchors, moved) : undefined;
53
+ if (!shift)
54
+ continue;
55
+ for (const f of run)
56
+ collapsed.add(f);
57
+ shifts.push(shift);
58
+ }
59
+ }
60
+ if (shifts.length === 0)
61
+ return facts;
62
+ return sortFacts([...facts.filter((f) => !collapsed.has(f)), ...shifts]);
63
+ }
64
+ /** One contiguous run as a shift on what explains it; nothing means the
65
+ * move stays as moves. In order of preference:
66
+ *
67
+ * 1. A move with another displacement that crossed the run — before it in
68
+ * the baseline, after it now, or the reverse. That is a reorder: moving
69
+ * item 3 to slot 9 of 12 moves items 4–9 up 37px each, and item 3 (the
70
+ * only thing the author touched) sits after them in the current order,
71
+ * so "nearest preceding" can never find it. Measured on the 12-item
72
+ * probe: before this, the six −37px moves stayed six (no anchor) and
73
+ * item 3's `moved down 222px` was the 8th line.
74
+ * 2. The nearest preceding element that changed in another way.
75
+ * 3. The nearest preceding move with another displacement.
76
+ *
77
+ * (1) and (3) take the head of the anchor's own block of equal moves, so the
78
+ * shift names the list item, not the `span` inside it that moved with it.
79
+ * (2) is ahead of (3) because a nearer odd move is usually a consequence of
80
+ * the same cause: in `inserted` the logo drops off the button's line between
81
+ * the banner and the rows, and the rows moved because of the banner. */
82
+ function collapse(run, anchors, moved) {
83
+ const first = run[0];
84
+ const last = run[run.length - 1];
85
+ const d = first.delta;
86
+ const other = moved.filter((f) => {
87
+ const m = f.delta;
88
+ return m.dx !== d.dx || m.dy !== d.dy;
89
+ });
90
+ const base = (f) => f.source.index; // moves are matched: baseline side
91
+ const crossed = other
92
+ .filter((f) => (base(f) < base(first) && f.order > last.order) ||
93
+ (base(f) > base(last) && f.order < first.order))
94
+ .sort((x, y) => base(x) - base(y));
95
+ const preceding = (fs) => fs.filter((f) => f.order < first.order).at(-1);
96
+ const anchor = (crossed[0] && headOf(crossed[0], moved)) ??
97
+ preceding(anchors) ??
98
+ (preceding(other) && headOf(preceding(other), moved));
99
+ if (!anchor)
100
+ return undefined;
101
+ return {
102
+ delta: {
103
+ kind: 'shift',
104
+ after: 'el' in anchor.delta ? anchor.delta.el : anchor.delta.after,
105
+ dx: d.dx,
106
+ dy: d.dy,
107
+ count: run.length,
108
+ },
109
+ rects: run.flatMap((f) => f.rects),
110
+ abstract: anchor.abstract,
111
+ order: anchor.order + 0.25,
112
+ source: anchor.source,
113
+ };
114
+ }
115
+ /** The first of the moves contiguous (in document order) with `f` that
116
+ * share its displacement: an element and the descendants that moved with it
117
+ * read as the element. */
118
+ function headOf(f, moved) {
119
+ const m = f.delta;
120
+ let k = moved.indexOf(f);
121
+ while (k > 0) {
122
+ const p = moved[k - 1].delta;
123
+ if (p.dx !== m.dx || p.dy !== m.dy)
124
+ break;
125
+ k--;
126
+ }
127
+ return moved[k];
128
+ }
129
+ /** Some anchor sits strictly between two run members. An anchor at a
130
+ * member's own position (a moved row whose text also changed) is part of
131
+ * the run, not a new cause. */
132
+ function causeBetween(orders, lo, hi) {
133
+ let a = 0;
134
+ let b = orders.length;
135
+ while (a < b) {
136
+ const m = (a + b) >> 1;
137
+ if (orders[m] <= lo)
138
+ a = m + 1;
139
+ else
140
+ b = m;
141
+ }
142
+ return a < orders.length && orders[a] < hi;
143
+ }
144
+ //# sourceMappingURL=shift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift.js","sourceRoot":"","sources":["../../../src/diff/structural/shift.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { ChangedRegion, Classification, Delta, Flag, StructureSnapshot, StyleTag } from '../../types.js';
2
+ export declare const MAX_SUMMARY = 300;
3
+ /** `change`: the summary's lead clauses without the per-story tail. */
4
+ export declare const MAX_CHANGE = 160;
5
+ export declare function describeDelta(d: Delta): string;
6
+ /** The deltas as indented display lines for a human, grouped by element:
7
+ * the element's content changes (four-side longhands collapsed), then its
8
+ * resize and move on one line. Groups run causes first — content, added and
9
+ * removed, and a move a shift is blamed on; then shifts; then other moves;
10
+ * then resizes, so an ancestor's consequential `resized` comes last. Every
11
+ * delta passed in is stated on some line; `total` is the verdict's
12
+ * `deltaCount`, which says whether `deltas` is the whole set. */
13
+ export declare function describeDeltas(deltas: Delta[], total?: number): string[];
14
+ export interface SummaryArgs {
15
+ classification: Classification;
16
+ styleTags: StyleTag[];
17
+ deltas: Delta[];
18
+ deltaCount: number;
19
+ regionCount: number;
20
+ unexplainedRegions: ChangedRegion[];
21
+ flags: Flag[];
22
+ imagesIdentical: boolean;
23
+ baseline?: StructureSnapshot;
24
+ current?: StructureSnapshot;
25
+ }
26
+ /** The change alone, for naming what several stories share: the summary's
27
+ * own clause wording with every element reduced to its role|tag (the
28
+ * caller passes `deltas` already abstracted), no coverage or tolerance
29
+ * tail, at most two clauses. */
30
+ export declare function buildChange(a: SummaryArgs): string;
31
+ export declare function buildSummary(a: SummaryArgs): string;