@variance-authority/presentation 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 (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +319 -0
  4. package/dist/alignment.d.ts +9 -0
  5. package/dist/alignment.js +100 -0
  6. package/dist/alignment.js.map +1 -0
  7. package/dist/analyze.d.ts +20 -0
  8. package/dist/analyze.js +132 -0
  9. package/dist/analyze.js.map +1 -0
  10. package/dist/browser-agent-entry.d.ts +2 -0
  11. package/dist/browser-agent-entry.js +8 -0
  12. package/dist/browser-agent-entry.js.map +1 -0
  13. package/dist/browser-agent.bundle.js +3587 -0
  14. package/dist/browser-agent.d.ts +37 -0
  15. package/dist/browser-agent.js +130 -0
  16. package/dist/browser-agent.js.map +1 -0
  17. package/dist/bundle.d.ts +3 -0
  18. package/dist/bundle.js +20 -0
  19. package/dist/bundle.js.map +1 -0
  20. package/dist/compare.d.ts +4 -0
  21. package/dist/compare.js +49 -0
  22. package/dist/compare.js.map +1 -0
  23. package/dist/findings.d.ts +5 -0
  24. package/dist/findings.js +285 -0
  25. package/dist/findings.js.map +1 -0
  26. package/dist/focus.d.ts +17 -0
  27. package/dist/focus.js +84 -0
  28. package/dist/focus.js.map +1 -0
  29. package/dist/graph.d.ts +11 -0
  30. package/dist/graph.js +145 -0
  31. package/dist/graph.js.map +1 -0
  32. package/dist/hierarchy.d.ts +4 -0
  33. package/dist/hierarchy.js +171 -0
  34. package/dist/hierarchy.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.js +8 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/math.d.ts +24 -0
  39. package/dist/math.js +124 -0
  40. package/dist/math.js.map +1 -0
  41. package/dist/measure.d.ts +15 -0
  42. package/dist/measure.js +216 -0
  43. package/dist/measure.js.map +1 -0
  44. package/dist/model.d.ts +346 -0
  45. package/dist/model.js +2 -0
  46. package/dist/model.js.map +1 -0
  47. package/dist/paint.d.ts +12 -0
  48. package/dist/paint.js +192 -0
  49. package/dist/paint.js.map +1 -0
  50. package/dist/patterns.d.ts +10 -0
  51. package/dist/patterns.js +148 -0
  52. package/dist/patterns.js.map +1 -0
  53. package/dist/playwright.d.ts +27 -0
  54. package/dist/playwright.js +108 -0
  55. package/dist/playwright.js.map +1 -0
  56. package/dist/report.d.ts +17 -0
  57. package/dist/report.js +126 -0
  58. package/dist/report.js.map +1 -0
  59. package/dist/spacing.d.ts +4 -0
  60. package/dist/spacing.js +112 -0
  61. package/dist/spacing.js.map +1 -0
  62. package/dist/telemetry.d.ts +4 -0
  63. package/dist/telemetry.js +73 -0
  64. package/dist/telemetry.js.map +1 -0
  65. package/mark.svg +30 -0
  66. package/package.json +62 -0
  67. package/skills/variance-presentation/SKILL.md +463 -0
@@ -0,0 +1,37 @@
1
+ import type { Digest, RawCapture, SubjectRef, Viewport } from '@variance-authority/core';
2
+ import type { PaintInstruction, PaintLayer } from './model.js';
3
+ export declare const PRESENTATION_AGENT = "__variance_authority_presentation__";
4
+ export declare const PRESENTATION_AGENT_VERSION = "presentation@0";
5
+ export declare const PRESENTATION_ROOT_ATTRIBUTE = "data-variance-authority-presentation-root";
6
+ export declare const PRESENTATION_OVERLAY_ATTRIBUTE = "data-variance-authority-presentation-overlay";
7
+ export interface PresentationAcquireRequest {
8
+ readonly subject: SubjectRef;
9
+ readonly viewport: Viewport;
10
+ readonly engine: string;
11
+ readonly fonts?: readonly string[];
12
+ readonly stabilize?: readonly string[];
13
+ readonly suspense?: {
14
+ readonly timeoutMs?: number;
15
+ };
16
+ }
17
+ export interface PresentationAcquired {
18
+ readonly capture: RawCapture;
19
+ readonly stabilization: {
20
+ readonly ids: readonly string[];
21
+ readonly digest?: Digest;
22
+ };
23
+ readonly portals: readonly {
24
+ readonly marker: string;
25
+ readonly previous?: string;
26
+ }[];
27
+ }
28
+ export interface InstalledPresentationAgent {
29
+ readonly acquire: (root: Element, request: PresentationAcquireRequest) => Promise<string>;
30
+ readonly paint: (instructions: readonly PaintInstruction[], layers?: readonly PaintLayer[]) => number;
31
+ readonly clear: () => void;
32
+ readonly version: string;
33
+ }
34
+ export declare function acquirePresentation(root: Element, request: PresentationAcquireRequest): Promise<string>;
35
+ export declare function paintPresentation(instructions: readonly PaintInstruction[], layers?: readonly PaintLayer[]): number;
36
+ export declare function clearPresentationPaint(): void;
37
+ //# sourceMappingURL=browser-agent.d.ts.map
@@ -0,0 +1,130 @@
1
+ import { COLLECT_RECIPE, recipeOf, } from '@variance-authority/core';
2
+ import { collect, stabilizeForObservation } from '@variance-authority/dom';
3
+ import { awaitSuspense, portalContentOf } from '@variance-authority/react';
4
+ export const PRESENTATION_AGENT = '__variance_authority_presentation__';
5
+ export const PRESENTATION_AGENT_VERSION = 'presentation@0';
6
+ export const PRESENTATION_ROOT_ATTRIBUTE = 'data-variance-authority-presentation-root';
7
+ export const PRESENTATION_OVERLAY_ATTRIBUTE = 'data-variance-authority-presentation-overlay';
8
+ let sequence = 0;
9
+ export async function acquirePresentation(root, request) {
10
+ clearPresentationPaint();
11
+ await awaitSuspense(root, request.suspense ?? {});
12
+ const portalRoots = portalContentOf(root);
13
+ const held = await stabilizeForObservation(root.ownerDocument, {
14
+ recipe: request.stabilize === undefined
15
+ ? presentationRecipe([root, ...portalRoots])
16
+ : recipeOf(request.stabilize),
17
+ });
18
+ const capture = collect(root, {
19
+ subject: request.subject,
20
+ viewport: request.viewport,
21
+ engine: request.engine,
22
+ portalsOf: () => portalRoots,
23
+ ...(request.fonts === undefined ? {} : { fonts: request.fonts }),
24
+ ...(held.digest === undefined ? {} : { stabilization: held.digest }),
25
+ });
26
+ const current = ++sequence;
27
+ const portals = portalRoots.map((portal, index) => {
28
+ const previous = portal.getAttribute(PRESENTATION_ROOT_ATTRIBUTE);
29
+ const marker = `${current}-${index}`;
30
+ portal.setAttribute(PRESENTATION_ROOT_ATTRIBUTE, marker);
31
+ return { marker, ...(previous === null ? {} : { previous }) };
32
+ });
33
+ return JSON.stringify({
34
+ capture,
35
+ stabilization: {
36
+ ids: held.ids,
37
+ ...(held.digest === undefined ? {} : { digest: held.digest }),
38
+ },
39
+ portals,
40
+ });
41
+ }
42
+ function presentationRecipe(roots) {
43
+ const waitForSubjectImages = {
44
+ id: 'wait-for-subject-images',
45
+ trick: 'wait',
46
+ needs: 'layout',
47
+ governs: 'images',
48
+ because: 'waited only for images inside the presentation subject and its portals',
49
+ settle: async () => {
50
+ const images = roots.flatMap((root) => [
51
+ ...(root.tagName.toLowerCase() === 'img' ? [root] : []),
52
+ ...Array.from(root.querySelectorAll('img')),
53
+ ]);
54
+ await Promise.all(images.filter((image) => !image.complete).map((image) => new Promise((resolve) => {
55
+ image.addEventListener('load', () => resolve(), { once: true });
56
+ image.addEventListener('error', () => resolve(), { once: true });
57
+ })));
58
+ },
59
+ };
60
+ return [
61
+ ...COLLECT_RECIPE.filter((intervention) => intervention.id !== 'wait-for-images'),
62
+ waitForSubjectImages,
63
+ ];
64
+ }
65
+ export function paintPresentation(instructions, layers) {
66
+ clearPresentationPaint();
67
+ const selected = layers === undefined ? undefined : new Set(layers);
68
+ const visible = instructions.filter((instruction) => selected?.has(instruction.layer) ?? true);
69
+ if (visible.length === 0)
70
+ return 0;
71
+ const document = globalThis.document;
72
+ const namespace = 'http://www.w3.org/2000/svg';
73
+ const svg = document.createElementNS(namespace, 'svg');
74
+ svg.setAttribute(PRESENTATION_OVERLAY_ATTRIBUTE, '');
75
+ svg.setAttribute('aria-hidden', 'true');
76
+ svg.setAttribute('width', String(Math.max(document.documentElement.scrollWidth, innerWidth)));
77
+ svg.setAttribute('height', String(Math.max(document.documentElement.scrollHeight, innerHeight)));
78
+ Object.assign(svg.style, {
79
+ position: 'absolute', left: '0', top: '0', pointerEvents: 'none',
80
+ overflow: 'visible', zIndex: '2147483647',
81
+ });
82
+ for (const instruction of visible) {
83
+ const group = document.createElementNS(namespace, 'g');
84
+ group.setAttribute('data-layer', instruction.layer);
85
+ group.setAttribute('data-measurement', instruction.id);
86
+ if (instruction.shape === 'rect' && instruction.rect !== undefined) {
87
+ const rectangle = document.createElementNS(namespace, 'rect');
88
+ rectangle.setAttribute('x', String(instruction.rect.x + scrollX));
89
+ rectangle.setAttribute('y', String(instruction.rect.y + scrollY));
90
+ rectangle.setAttribute('width', String(instruction.rect.width));
91
+ rectangle.setAttribute('height', String(instruction.rect.height));
92
+ rectangle.setAttribute('fill', 'none');
93
+ rectangle.setAttribute('stroke', instruction.color);
94
+ rectangle.setAttribute('stroke-width', instruction.layer === 'findings' ? '3' : '1');
95
+ rectangle.setAttribute('stroke-dasharray', instruction.layer === 'semantic' ? '3 2' : 'none');
96
+ group.append(rectangle);
97
+ group.append(paintLabel(instruction.label, instruction.rect.x + scrollX + 2, instruction.rect.y + scrollY + 11));
98
+ }
99
+ else if (instruction.shape === 'line' && instruction.line !== undefined) {
100
+ const line = document.createElementNS(namespace, 'line');
101
+ line.setAttribute('x1', String(instruction.line.x1 + scrollX));
102
+ line.setAttribute('y1', String(instruction.line.y1 + scrollY));
103
+ line.setAttribute('x2', String(instruction.line.x2 + scrollX));
104
+ line.setAttribute('y2', String(instruction.line.y2 + scrollY));
105
+ line.setAttribute('stroke', instruction.color);
106
+ group.append(line);
107
+ group.append(paintLabel(instruction.label, (instruction.line.x1 + instruction.line.x2) / 2 + scrollX, (instruction.line.y1 + instruction.line.y2) / 2 + scrollY));
108
+ }
109
+ svg.append(group);
110
+ }
111
+ document.documentElement.append(svg);
112
+ return visible.length;
113
+ }
114
+ export function clearPresentationPaint() {
115
+ globalThis.document?.querySelector(`[${PRESENTATION_OVERLAY_ATTRIBUTE}]`)?.remove();
116
+ }
117
+ function paintLabel(text, x, y) {
118
+ const value = document.createElementNS('http://www.w3.org/2000/svg', 'text');
119
+ value.setAttribute('x', String(x));
120
+ value.setAttribute('y', String(y));
121
+ value.setAttribute('fill', '#111');
122
+ value.setAttribute('stroke', '#fff');
123
+ value.setAttribute('stroke-width', '3');
124
+ value.setAttribute('paint-order', 'stroke');
125
+ value.setAttribute('font-family', 'ui-monospace, monospace');
126
+ value.setAttribute('font-size', '10');
127
+ value.textContent = text;
128
+ return value;
129
+ }
130
+ //# sourceMappingURL=browser-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-agent.js","sourceRoot":"","sources":["../src/browser-agent.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n COLLECT_RECIPE,\n recipeOf,\n} from '@variance-authority/core';\nimport type {\n Digest,\n RawCapture,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core';\nimport { collect, stabilizeForObservation } from '@variance-authority/dom';\nimport { awaitSuspense, portalContentOf } from '@variance-authority/react';\nimport type { PaintInstruction, PaintLayer } from './model.js';\n\nexport const PRESENTATION_AGENT = '__variance_authority_presentation__';\nexport const PRESENTATION_AGENT_VERSION = 'presentation@0';\nexport const PRESENTATION_ROOT_ATTRIBUTE = 'data-variance-authority-presentation-root';\nexport const PRESENTATION_OVERLAY_ATTRIBUTE = 'data-variance-authority-presentation-overlay';\nlet sequence = 0;\n\nexport interface PresentationAcquireRequest {\n readonly subject: SubjectRef;\n readonly viewport: Viewport;\n readonly engine: string;\n readonly fonts?: readonly string[];\n readonly stabilize?: readonly string[];\n readonly suspense?: { readonly timeoutMs?: number };\n}\n\nexport interface PresentationAcquired {\n readonly capture: RawCapture;\n readonly stabilization: { readonly ids: readonly string[]; readonly digest?: Digest };\n readonly portals: readonly { readonly marker: string; readonly previous?: string }[];\n}\n\nexport interface InstalledPresentationAgent {\n readonly acquire: (root: Element, request: PresentationAcquireRequest) => Promise<string>;\n readonly paint: (instructions: readonly PaintInstruction[], layers?: readonly PaintLayer[]) => number;\n readonly clear: () => void;\n readonly version: string;\n}\n\nexport async function acquirePresentation(\n root: Element,\n request: PresentationAcquireRequest,\n): Promise<string> {\n clearPresentationPaint();\n await awaitSuspense(root, request.suspense ?? {});\n const portalRoots = portalContentOf(root);\n const held = await stabilizeForObservation(\n root.ownerDocument,\n {\n recipe: request.stabilize === undefined\n ? presentationRecipe([root, ...portalRoots])\n : recipeOf(request.stabilize),\n },\n );\n const capture = collect(root, {\n subject: request.subject,\n viewport: request.viewport,\n engine: request.engine,\n portalsOf: () => portalRoots,\n ...(request.fonts === undefined ? {} : { fonts: request.fonts }),\n ...(held.digest === undefined ? {} : { stabilization: held.digest }),\n });\n const current = ++sequence;\n const portals = portalRoots.map((portal, index) => {\n const previous = portal.getAttribute(PRESENTATION_ROOT_ATTRIBUTE);\n const marker = `${current}-${index}`;\n portal.setAttribute(PRESENTATION_ROOT_ATTRIBUTE, marker);\n return { marker, ...(previous === null ? {} : { previous }) };\n });\n return JSON.stringify({\n capture,\n stabilization: {\n ids: held.ids,\n ...(held.digest === undefined ? {} : { digest: held.digest }),\n },\n portals,\n });\n}\n\nfunction presentationRecipe(roots: readonly Element[]) {\n const waitForSubjectImages = {\n id: 'wait-for-subject-images',\n trick: 'wait',\n needs: 'layout',\n governs: 'images',\n because: 'waited only for images inside the presentation subject and its portals',\n settle: async () => {\n const images = roots.flatMap((root) => [\n ...(root.tagName.toLowerCase() === 'img' ? [root as HTMLImageElement] : []),\n ...Array.from(root.querySelectorAll('img')),\n ]);\n await Promise.all(images.filter((image) => !image.complete).map((image) => new Promise<void>((resolve) => {\n image.addEventListener('load', () => resolve(), { once: true });\n image.addEventListener('error', () => resolve(), { once: true });\n })));\n },\n } as const;\n return [\n ...COLLECT_RECIPE.filter((intervention) => intervention.id !== 'wait-for-images'),\n waitForSubjectImages,\n ];\n}\n\nexport function paintPresentation(\n instructions: readonly PaintInstruction[],\n layers?: readonly PaintLayer[],\n): number {\n clearPresentationPaint();\n const selected = layers === undefined ? undefined : new Set(layers);\n const visible = instructions.filter((instruction) => selected?.has(instruction.layer) ?? true);\n if (visible.length === 0) return 0;\n const document = globalThis.document;\n const namespace = 'http://www.w3.org/2000/svg';\n const svg = document.createElementNS(namespace, 'svg');\n svg.setAttribute(PRESENTATION_OVERLAY_ATTRIBUTE, '');\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('width', String(Math.max(document.documentElement.scrollWidth, innerWidth)));\n svg.setAttribute('height', String(Math.max(document.documentElement.scrollHeight, innerHeight)));\n Object.assign(svg.style, {\n position: 'absolute', left: '0', top: '0', pointerEvents: 'none',\n overflow: 'visible', zIndex: '2147483647',\n });\n\n for (const instruction of visible) {\n const group = document.createElementNS(namespace, 'g');\n group.setAttribute('data-layer', instruction.layer);\n group.setAttribute('data-measurement', instruction.id);\n if (instruction.shape === 'rect' && instruction.rect !== undefined) {\n const rectangle = document.createElementNS(namespace, 'rect');\n rectangle.setAttribute('x', String(instruction.rect.x + scrollX));\n rectangle.setAttribute('y', String(instruction.rect.y + scrollY));\n rectangle.setAttribute('width', String(instruction.rect.width));\n rectangle.setAttribute('height', String(instruction.rect.height));\n rectangle.setAttribute('fill', 'none');\n rectangle.setAttribute('stroke', instruction.color);\n rectangle.setAttribute('stroke-width', instruction.layer === 'findings' ? '3' : '1');\n rectangle.setAttribute('stroke-dasharray', instruction.layer === 'semantic' ? '3 2' : 'none');\n group.append(rectangle);\n group.append(paintLabel(instruction.label, instruction.rect.x + scrollX + 2, instruction.rect.y + scrollY + 11));\n } else if (instruction.shape === 'line' && instruction.line !== undefined) {\n const line = document.createElementNS(namespace, 'line');\n line.setAttribute('x1', String(instruction.line.x1 + scrollX));\n line.setAttribute('y1', String(instruction.line.y1 + scrollY));\n line.setAttribute('x2', String(instruction.line.x2 + scrollX));\n line.setAttribute('y2', String(instruction.line.y2 + scrollY));\n line.setAttribute('stroke', instruction.color);\n group.append(line);\n group.append(paintLabel(\n instruction.label,\n (instruction.line.x1 + instruction.line.x2) / 2 + scrollX,\n (instruction.line.y1 + instruction.line.y2) / 2 + scrollY,\n ));\n }\n svg.append(group);\n }\n document.documentElement.append(svg);\n return visible.length;\n}\n\nexport function clearPresentationPaint(): void {\n globalThis.document?.querySelector(`[${PRESENTATION_OVERLAY_ATTRIBUTE}]`)?.remove();\n}\n\nfunction paintLabel(text: string, x: number, y: number): SVGTextElement {\n const value = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n value.setAttribute('x', String(x));\n value.setAttribute('y', String(y));\n value.setAttribute('fill', '#111');\n value.setAttribute('stroke', '#fff');\n value.setAttribute('stroke-width', '3');\n value.setAttribute('paint-order', 'stroke');\n value.setAttribute('font-family', 'ui-monospace, monospace');\n value.setAttribute('font-size', '10');\n value.textContent = text;\n return value;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Read the prebuilt sensing agent installed into the inspected page. */
2
+ export declare function bundlePresentationAgent(): Promise<string>;
3
+ //# sourceMappingURL=bundle.d.ts.map
package/dist/bundle.js ADDED
@@ -0,0 +1,20 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ /** Read the prebuilt sensing agent installed into the inspected page. */
4
+ export async function bundlePresentationAgent() {
5
+ const candidates = [
6
+ new URL('./browser-agent.bundle.js', import.meta.url),
7
+ new URL('../dist/browser-agent.bundle.js', import.meta.url),
8
+ ];
9
+ for (const path of candidates) {
10
+ try {
11
+ return await readFile(path, 'utf8');
12
+ }
13
+ catch {
14
+ // Source tests and installed consumers reach different emitted locations.
15
+ }
16
+ }
17
+ throw new Error(`the presentation sensing agent bundle is missing (looked in ` +
18
+ `${candidates.map((path) => fileURLToPath(path)).join(' and ')}); run the package build first`);
19
+ }
20
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,uBAAuB;IAC3C,MAAM,UAAU,GAAG;QACjB,IAAI,GAAG,CAAC,2BAA2B,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC;QACrD,IAAI,GAAG,CAAC,iCAAiC,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC;KAC5D,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,0EAA0E;QAC5E,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,8DAA8D;QAC5D,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gCAAgC,CACjG,CAAC;AACJ,CAAC","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\n\n/** Read the prebuilt sensing agent installed into the inspected page. */\nexport async function bundlePresentationAgent(): Promise<string> {\n const candidates = [\n new URL('./browser-agent.bundle.js', import.meta.url),\n new URL('../dist/browser-agent.bundle.js', import.meta.url),\n ];\n for (const path of candidates) {\n try {\n return await readFile(path, 'utf8');\n } catch {\n // Source tests and installed consumers reach different emitted locations.\n }\n }\n throw new Error(\n `the presentation sensing agent bundle is missing (looked in ` +\n `${candidates.map((path) => fileURLToPath(path)).join(' and ')}); run the package build first`,\n );\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { PresentationComparison, PresentationReport } from './model.js';
2
+ /** Compare independent presentation dimensions without collapsing them into a score or verdict. */
3
+ export declare function comparePresentation(before: PresentationReport, after: PresentationReport): PresentationComparison;
4
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { digestValue } from '@variance-authority/core';
2
+ /** Compare independent presentation dimensions without collapsing them into a score or verdict. */
3
+ export function comparePresentation(before, after) {
4
+ const beforeContent = contentIdentity(before);
5
+ const afterContent = contentIdentity(after);
6
+ const findings = before.findings === undefined || after.findings === undefined
7
+ ? undefined
8
+ : findingRules(before, after).map((rule) => {
9
+ const left = before.findings.filter((finding) => finding.rule === rule).length;
10
+ const right = after.findings.filter((finding) => finding.rule === rule).length;
11
+ return { rule, before: left, after: right, delta: right - left };
12
+ });
13
+ return {
14
+ formatVersion: 1,
15
+ before: before.digest,
16
+ after: after.digest,
17
+ ...(findings === undefined ? {} : { findings }),
18
+ information: {
19
+ content: {
20
+ before: beforeContent,
21
+ after: afterContent,
22
+ preserved: beforeContent === afterContent,
23
+ },
24
+ characters: {
25
+ before: before.telemetry.content.characters,
26
+ after: after.telemetry.content.characters,
27
+ },
28
+ elements: {
29
+ before: before.telemetry.content.elements,
30
+ after: after.telemetry.content.elements,
31
+ },
32
+ repeatedObjects: {
33
+ before: before.telemetry.content.repeatedObjects,
34
+ after: after.telemetry.content.repeatedObjects,
35
+ },
36
+ },
37
+ };
38
+ }
39
+ function contentIdentity(report) {
40
+ return digestValue({
41
+ content: report.contentDigest,
42
+ browserAccessibility: report.semantic.browserAccessibility,
43
+ });
44
+ }
45
+ function findingRules(before, after) {
46
+ return [...new Set([...(before.findings ?? []), ...(after.findings ?? [])].map((finding) => finding.rule))]
47
+ .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
48
+ }
49
+ //# sourceMappingURL=compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare.js","sourceRoot":"","sources":["../src/compare.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mGAAmG;AACnG,MAAM,UAAU,mBAAmB,CACjC,MAA0B,EAC1B,KAAyB;IAEzB,MAAM,aAAa,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,QAAQ,GACZ,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAC3D,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACvC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAS,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC;YAChF,MAAM,KAAK,GAAG,KAAK,CAAC,QAAS,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC;YAChF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,CAAC;QACnE,CAAC,CAAC,CAAC;IACT,OAAO;QACL,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,KAAK,CAAC,MAAM;QACnB,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,WAAW,EAAE;YACX,OAAO,EAAE;gBACP,MAAM,EAAE,aAAa;gBACrB,KAAK,EAAE,YAAY;gBACnB,SAAS,EAAE,aAAa,KAAK,YAAY;aAC1C;YACD,UAAU,EAAE;gBACV,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU;gBAC3C,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU;aAC1C;YACD,QAAQ,EAAE;gBACR,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ;gBACzC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ;aACxC;YACD,eAAe,EAAE;gBACf,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,eAAe;gBAChD,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,eAAe;aAC/C;SACF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,MAA0B;IACjD,OAAO,WAAW,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC,aAAa;QAC7B,oBAAoB,EAAE,MAAM,CAAC,QAAQ,CAAC,oBAAoB;KAClD,CAAC,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CACnB,MAA0B,EAC1B,KAAyB;IAEzB,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;SACxG,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import { digestValue } from '@variance-authority/core';\nimport type { PresentationComparison, PresentationFindingRule, PresentationReport } from './model.js';\n\n/** Compare independent presentation dimensions without collapsing them into a score or verdict. */\nexport function comparePresentation(\n before: PresentationReport,\n after: PresentationReport,\n): PresentationComparison {\n const beforeContent = contentIdentity(before);\n const afterContent = contentIdentity(after);\n const findings =\n before.findings === undefined || after.findings === undefined\n ? undefined\n : findingRules(before, after).map((rule) => {\n const left = before.findings!.filter((finding) => finding.rule === rule).length;\n const right = after.findings!.filter((finding) => finding.rule === rule).length;\n return { rule, before: left, after: right, delta: right - left };\n });\n return {\n formatVersion: 1,\n before: before.digest,\n after: after.digest,\n ...(findings === undefined ? {} : { findings }),\n information: {\n content: {\n before: beforeContent,\n after: afterContent,\n preserved: beforeContent === afterContent,\n },\n characters: {\n before: before.telemetry.content.characters,\n after: after.telemetry.content.characters,\n },\n elements: {\n before: before.telemetry.content.elements,\n after: after.telemetry.content.elements,\n },\n repeatedObjects: {\n before: before.telemetry.content.repeatedObjects,\n after: after.telemetry.content.repeatedObjects,\n },\n },\n };\n}\n\nfunction contentIdentity(report: PresentationReport): ReturnType<typeof digestValue> {\n return digestValue({\n content: report.contentDigest,\n browserAccessibility: report.semantic.browserAccessibility,\n } as never);\n}\n\nfunction findingRules(\n before: PresentationReport,\n after: PresentationReport,\n): PresentationFindingRule[] {\n return [...new Set([...(before.findings ?? []), ...(after.findings ?? [])].map((finding) => finding.rule))]\n .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { BuiltGraph } from './graph.js';
2
+ import type { Measurements } from './measure.js';
3
+ import type { PresentationFinding, RepeatedPattern } from './model.js';
4
+ export declare function detectFindings(graph: BuiltGraph, measured: Measurements, patterns: readonly RepeatedPattern[]): PresentationFinding[];
5
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1,285 @@
1
+ import { codeUnitCompare, median, round } from './math.js';
2
+ /** Thresholds are pinned by the acceptance fixtures, never user-facing targets. */
3
+ const CALIBRATION = {
4
+ relationshipRatio: 1.15,
5
+ weakBoundary: 0.28,
6
+ surfaceDelta: 8,
7
+ axisDeviationPx: 2,
8
+ baselineDeviationPx: 2,
9
+ };
10
+ export function detectFindings(graph, measured, patterns) {
11
+ const findings = [];
12
+ for (const pattern of patterns) {
13
+ findings.push(...relationshipFindings(pattern, measured));
14
+ findings.push(...hierarchySpacingFindings(pattern, measured));
15
+ findings.push(...slotFindings(pattern, graph));
16
+ const unexplained = pattern.outliers.filter((outlier) => !outlier.explainedByState);
17
+ if (pattern.instances.length >= 3 &&
18
+ pattern.dominantInstances.length * 2 >= pattern.instances.length &&
19
+ unexplained.length > 0) {
20
+ findings.push({
21
+ rule: 'PRESENTATION_GRAMMAR_DRIFT',
22
+ owner: pattern.parent,
23
+ nodes: unexplained.map((outlier) => outlier.node),
24
+ pattern: pattern.id,
25
+ measurements: {
26
+ dominantInstances: pattern.dominantInstances.length,
27
+ peerInstances: pattern.instances.length,
28
+ maxDeviation: round(Math.max(...unexplained.map((outlier) => outlier.deviation))),
29
+ },
30
+ });
31
+ }
32
+ }
33
+ findings.push(...surfaceFindings(measured.nodes));
34
+ findings.push(...prominenceFindings(measured));
35
+ return findings.sort((left, right) => codeUnitCompare(left.rule, right.rule) ||
36
+ codeUnitCompare(left.owner, right.owner) ||
37
+ codeUnitCompare(left.nodes.join('\0'), right.nodes.join('\0')) ||
38
+ codeUnitCompare(left.pattern ?? '', right.pattern ?? '')).map((finding, index) => ({ id: `F${index + 1}`, ...finding }));
39
+ }
40
+ /** A leading structural label and the body peers it introduces need distinct spacing relations. */
41
+ function hierarchySpacingFindings(pattern, measured) {
42
+ const byId = new Map(measured.nodes.map((node) => [node.id, node]));
43
+ const separations = new Map(measured.relations
44
+ .filter((relation) => relation.kind === 'separates')
45
+ .map((relation) => [`${relation.from}\0${relation.to}`, relation]));
46
+ const collisions = pattern.instances.flatMap((instanceId) => {
47
+ const instance = byId.get(instanceId);
48
+ if (instance === undefined || instance.children.length < 3)
49
+ return [];
50
+ const leader = byId.get(instance.children[0]);
51
+ const body = instance.children.slice(1).map((id) => byId.get(id));
52
+ if (leader === undefined || body.some((node) => node === undefined))
53
+ return [];
54
+ const bodyNodes = body;
55
+ if (bodyNodes.length < 2 ||
56
+ bodyNodes.some((node) => node.semanticClass !== bodyNodes[0].semanticClass) ||
57
+ leader.semanticClass === bodyNodes[0].semanticClass)
58
+ return [];
59
+ const leading = separations.get(`${leader.id}\0${bodyNodes[0].id}`);
60
+ const peer = bodyNodes.slice(0, -1).flatMap((node, index) => separations.get(`${node.id}\0${bodyNodes[index + 1].id}`) ?? []);
61
+ const leadingGap = leading?.distancePx;
62
+ const peerGap = median(peer.flatMap((relation) => relation.distancePx ?? []));
63
+ if (leadingGap === undefined || peerGap === undefined)
64
+ return [];
65
+ const leadingClusters = new Set(leading?.spacingCluster === undefined ? [] : [leading.spacingCluster]);
66
+ const peerClusters = new Set(peer.flatMap((relation) => relation.spacingCluster ?? []));
67
+ const sharedCluster = [...leadingClusters].some((cluster) => peerClusters.has(cluster));
68
+ const smaller = Math.min(leadingGap, peerGap);
69
+ const larger = Math.max(leadingGap, peerGap);
70
+ const ratio = smaller === 0 ? (larger === 0 ? 1 : Number.POSITIVE_INFINITY) : larger / smaller;
71
+ if (!sharedCluster && ratio > CALIBRATION.relationshipRatio)
72
+ return [];
73
+ return [{
74
+ instance: instance.id,
75
+ leader: leader.id,
76
+ body: bodyNodes.map((node) => node.id),
77
+ leadingGap,
78
+ peerGap,
79
+ ratio,
80
+ sharedCluster,
81
+ }];
82
+ });
83
+ if (collisions.length < 2)
84
+ return [];
85
+ return [{
86
+ rule: 'SPACING_HIERARCHY_COLLISION',
87
+ owner: pattern.parent,
88
+ nodes: collisions.map((collision) => collision.instance),
89
+ pattern: pattern.id,
90
+ measurements: {
91
+ instances: collisions.length,
92
+ leadingToBodyGapMedianPx: round(median(collisions.map((collision) => collision.leadingGap)), 2),
93
+ bodyToBodyGapMedianPx: round(median(collisions.map((collision) => collision.peerGap)), 2),
94
+ ratio: round(median(collisions.map((collision) => collision.ratio))),
95
+ sharedSpacingClusterInstances: collisions.filter((collision) => collision.sharedCluster).length,
96
+ },
97
+ }];
98
+ }
99
+ function relationshipFindings(pattern, measured) {
100
+ const members = new Set(pattern.instances);
101
+ const between = measured.relations.filter((relation) => relation.kind === 'separates' && members.has(relation.from) && members.has(relation.to));
102
+ const within = pattern.instances.flatMap((instance) => {
103
+ const node = measured.nodes.find((candidate) => candidate.id === instance);
104
+ if (node === undefined)
105
+ return [];
106
+ const children = new Set(node.children);
107
+ return measured.relations.filter((relation) => relation.kind === 'separates' && children.has(relation.from) && children.has(relation.to));
108
+ });
109
+ const betweenBoundary = median(between.flatMap((relation) => relation.boundaryStrength ?? []));
110
+ const withinBoundary = median(within.flatMap((relation) => relation.boundaryStrength ?? []));
111
+ const betweenGap = median(between.flatMap((relation) => relation.distancePx ?? []));
112
+ const withinGap = median(within.flatMap((relation) => relation.distancePx ?? []));
113
+ const results = [];
114
+ if (betweenBoundary !== undefined && withinBoundary !== undefined) {
115
+ const ratio = withinBoundary === 0 ? (betweenBoundary === 0 ? 1 : Number.POSITIVE_INFINITY) : betweenBoundary / withinBoundary;
116
+ if (ratio <= CALIBRATION.relationshipRatio) {
117
+ results.push({
118
+ rule: 'SEPARATION_COLLISION',
119
+ owner: pattern.parent,
120
+ nodes: pattern.instances,
121
+ pattern: pattern.id,
122
+ measurements: {
123
+ withinBoundaryMedian: round(withinBoundary),
124
+ betweenBoundaryMedian: round(betweenBoundary),
125
+ ratio: round(ratio),
126
+ },
127
+ });
128
+ }
129
+ if (pattern.instances.length >= 3 && betweenBoundary <= CALIBRATION.weakBoundary) {
130
+ results.push({
131
+ rule: 'REPETITION_GRAMMAR_COLLAPSE',
132
+ owner: pattern.parent,
133
+ nodes: pattern.instances,
134
+ pattern: pattern.id,
135
+ measurements: {
136
+ instances: pattern.instances.length,
137
+ betweenBoundaryMedian: round(betweenBoundary),
138
+ presentationSimilarity: pattern.presentationSimilarity,
139
+ },
140
+ });
141
+ }
142
+ }
143
+ if (betweenGap !== undefined && withinGap !== undefined) {
144
+ const ratio = withinGap === 0 ? (betweenGap === 0 ? 1 : Number.POSITIVE_INFINITY) : betweenGap / withinGap;
145
+ const betweenClusters = new Set(between.flatMap((relation) => relation.spacingCluster ?? []));
146
+ const withinClusters = new Set(within.flatMap((relation) => relation.spacingCluster ?? []));
147
+ const overlap = [...betweenClusters].some((cluster) => withinClusters.has(cluster));
148
+ if (overlap || ratio <= CALIBRATION.relationshipRatio) {
149
+ results.push({
150
+ rule: 'SPACING_RELATION_COLLISION',
151
+ owner: pattern.parent,
152
+ nodes: pattern.instances,
153
+ pattern: pattern.id,
154
+ measurements: {
155
+ withinGapMedianPx: round(withinGap, 2),
156
+ betweenGapMedianPx: round(betweenGap, 2),
157
+ ratio: round(ratio),
158
+ },
159
+ });
160
+ }
161
+ }
162
+ return results;
163
+ }
164
+ function slotFindings(pattern, graph) {
165
+ if (pattern.instances.length < 4)
166
+ return [];
167
+ const slots = new Map();
168
+ for (const instanceId of pattern.instances) {
169
+ const instance = graph.byId.get(instanceId);
170
+ if (instance === undefined)
171
+ continue;
172
+ const occurrence = new Map();
173
+ for (const childId of instance.children) {
174
+ const child = graph.byId.get(childId);
175
+ if (child === undefined)
176
+ continue;
177
+ const count = occurrence.get(child.semanticClass) ?? 0;
178
+ occurrence.set(child.semanticClass, count + 1);
179
+ const key = `${child.semanticClass}:${count}`;
180
+ const members = slots.get(key) ?? [];
181
+ members.push({ node: child, instance });
182
+ slots.set(key, members);
183
+ }
184
+ }
185
+ const findings = [];
186
+ for (const [slot, members] of slots) {
187
+ if (members.length !== pattern.instances.length)
188
+ continue;
189
+ const left = median(members.map((member) => member.node.rect.x));
190
+ const aligned = members.filter((member) => Math.abs(member.node.rect.x - left) <= 1);
191
+ const alignmentOutliers = members.filter((member) => Math.abs(member.node.rect.x - left) > CALIBRATION.axisDeviationPx);
192
+ if (aligned.length * 3 >= members.length * 2 && alignmentOutliers.length > 0) {
193
+ findings.push({
194
+ rule: 'ALIGNMENT_OUTLIER',
195
+ owner: pattern.parent,
196
+ nodes: alignmentOutliers.map((member) => member.node.id),
197
+ pattern: pattern.id,
198
+ measurements: {
199
+ slot,
200
+ dominantLeftPx: round(left, 2),
201
+ maxDeviationPx: round(Math.max(...alignmentOutliers.map((member) => Math.abs(member.node.rect.x - left))), 2),
202
+ },
203
+ });
204
+ }
205
+ const baselines = members.flatMap(({ node, instance }) => {
206
+ const size = node.typography.sizePx;
207
+ const lineHeight = node.typography.lineHeightPx;
208
+ if (size === undefined || lineHeight === undefined || (node.text === undefined && node.name === undefined))
209
+ return [];
210
+ return [{
211
+ node,
212
+ coordinate: node.rect.y - instance.rect.y + Math.max(0, (node.rect.height - lineHeight) / 2) + size * 0.8,
213
+ }];
214
+ });
215
+ if (baselines.length !== members.length)
216
+ continue;
217
+ const baseline = median(baselines.map((candidate) => candidate.coordinate));
218
+ const baselineAligned = baselines.filter((candidate) => Math.abs(candidate.coordinate - baseline) <= 1);
219
+ const baselineOutliers = baselines.filter((candidate) => Math.abs(candidate.coordinate - baseline) > CALIBRATION.baselineDeviationPx);
220
+ if (baselineAligned.length * 3 >= baselines.length * 2 && baselineOutliers.length > 0) {
221
+ findings.push({
222
+ rule: 'BASELINE_DRIFT',
223
+ owner: pattern.parent,
224
+ nodes: baselineOutliers.map((candidate) => candidate.node.id),
225
+ pattern: pattern.id,
226
+ measurements: {
227
+ slot,
228
+ dominantBaselinePx: round(baseline, 2),
229
+ maxDeviationPx: round(Math.max(...baselineOutliers.map((candidate) => Math.abs(candidate.coordinate - baseline))), 2),
230
+ confidence: 'inferred',
231
+ },
232
+ });
233
+ }
234
+ }
235
+ return findings;
236
+ }
237
+ function surfaceFindings(nodes) {
238
+ const surfaceTags = new Set(['button', 'input', 'select', 'textarea', 'article', 'section', 'aside', 'dialog']);
239
+ return nodes.flatMap((node) => {
240
+ const difference = node.surface.perceptualDifference;
241
+ const meaningful = node.role !== undefined || surfaceTags.has(node.tag);
242
+ if (node.parent === undefined ||
243
+ !meaningful ||
244
+ node.surface.fill === undefined ||
245
+ difference === undefined ||
246
+ difference >= CALIBRATION.surfaceDelta ||
247
+ node.surface.borderWidthPx > 0 ||
248
+ node.surface.shadow)
249
+ return [];
250
+ return [{
251
+ rule: 'SURFACE_COLLISION',
252
+ owner: node.parent,
253
+ nodes: [node.id],
254
+ measurements: {
255
+ perceptualDifference: difference,
256
+ borderWidthPx: node.surface.borderWidthPx,
257
+ shadow: node.surface.shadow ? 1 : 0,
258
+ },
259
+ }];
260
+ });
261
+ }
262
+ function prominenceFindings(measured) {
263
+ const byId = new Map(measured.nodes.map((node) => [node.id, node]));
264
+ return measured.nodes.flatMap((owner) => measured.prominence.flatMap((cluster) => {
265
+ const members = owner.children.flatMap((id) => {
266
+ const node = byId.get(id);
267
+ return node !== undefined && node.prominence.cluster === cluster.id ? [node] : [];
268
+ });
269
+ const headings = members.filter((node) => node.semanticClass.startsWith('heading'));
270
+ const ordinary = members.filter((node) => ['label', 'paragraph', 'text', 'generic', 'span'].includes(node.semanticClass));
271
+ if (headings.length === 0 || ordinary.length === 0)
272
+ return [];
273
+ return [{
274
+ rule: 'PROMINENCE_COLLAPSE',
275
+ owner: owner.id,
276
+ nodes: [...headings, ...ordinary].map((node) => node.id),
277
+ measurements: {
278
+ prominenceCluster: cluster.id,
279
+ semanticClasses: [...new Set([...headings, ...ordinary].map((node) => node.semanticClass))].join(','),
280
+ magnitude: cluster.magnitude,
281
+ },
282
+ }];
283
+ }));
284
+ }
285
+ //# sourceMappingURL=findings.js.map