@setsquare/review-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/README.md +338 -0
- package/THIRD-PARTY-NOTICES.md +61 -0
- package/dist/autoscan.d.ts +25 -0
- package/dist/autoscan.d.ts.map +1 -0
- package/dist/autoscan.js +332 -0
- package/dist/autoscan.js.map +1 -0
- package/dist/axe.d.ts +96 -0
- package/dist/axe.d.ts.map +1 -0
- package/dist/axe.js +210 -0
- package/dist/axe.js.map +1 -0
- package/dist/bin/setsquare.d.ts +3 -0
- package/dist/bin/setsquare.d.ts.map +1 -0
- package/dist/bin/setsquare.js +21 -0
- package/dist/bin/setsquare.js.map +1 -0
- package/dist/brand.d.ts +84 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +335 -0
- package/dist/brand.js.map +1 -0
- package/dist/checks/dynamic.d.ts +57 -0
- package/dist/checks/dynamic.d.ts.map +1 -0
- package/dist/checks/dynamic.js +184 -0
- package/dist/checks/dynamic.js.map +1 -0
- package/dist/checks/rule-pack.d.ts +34 -0
- package/dist/checks/rule-pack.d.ts.map +1 -0
- package/dist/checks/rule-pack.js +270 -0
- package/dist/checks/rule-pack.js.map +1 -0
- package/dist/checks/versions.d.ts +28 -0
- package/dist/checks/versions.d.ts.map +1 -0
- package/dist/checks/versions.js +45 -0
- package/dist/checks/versions.js.map +1 -0
- package/dist/ci.d.ts +37 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +199 -0
- package/dist/ci.js.map +1 -0
- package/dist/contract.d.ts +334 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +35 -0
- package/dist/contract.js.map +1 -0
- package/dist/dedupe.d.ts +79 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +102 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/document.d.ts +29 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +94 -0
- package/dist/document.js.map +1 -0
- package/dist/envelope.d.ts +124 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +337 -0
- package/dist/envelope.js.map +1 -0
- package/dist/escape.d.ts +14 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +20 -0
- package/dist/escape.js.map +1 -0
- package/dist/findings.d.ts +47 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +229 -0
- package/dist/findings.js.map +1 -0
- package/dist/fixture.d.ts +7 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +237 -0
- package/dist/fixture.js.map +1 -0
- package/dist/group.d.ts +214 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +709 -0
- package/dist/group.js.map +1 -0
- package/dist/html-report.d.ts +68 -0
- package/dist/html-report.d.ts.map +1 -0
- package/dist/html-report.js +649 -0
- package/dist/html-report.js.map +1 -0
- package/dist/human.d.ts +65 -0
- package/dist/human.d.ts.map +1 -0
- package/dist/human.js +111 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +65 -0
- package/dist/infer.d.ts.map +1 -0
- package/dist/infer.js +67 -0
- package/dist/infer.js.map +1 -0
- package/dist/insights.d.ts +140 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +483 -0
- package/dist/insights.js.map +1 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +114 -0
- package/dist/install.js.map +1 -0
- package/dist/links.d.ts +45 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +45 -0
- package/dist/links.js.map +1 -0
- package/dist/mask.d.ts +85 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +98 -0
- package/dist/mask.js.map +1 -0
- package/dist/observe.d.ts +51 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +233 -0
- package/dist/observe.js.map +1 -0
- package/dist/options.d.ts +249 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +185 -0
- package/dist/options.js.map +1 -0
- package/dist/output.d.ts +113 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +231 -0
- package/dist/output.js.map +1 -0
- package/dist/perf.d.ts +11 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +150 -0
- package/dist/perf.js.map +1 -0
- package/dist/reading-level.d.ts +73 -0
- package/dist/reading-level.d.ts.map +1 -0
- package/dist/reading-level.js +189 -0
- package/dist/reading-level.js.map +1 -0
- package/dist/reporter.d.ts +94 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +709 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules.d.ts +55 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +77 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +32 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +67 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sanitise.d.ts +116 -0
- package/dist/sanitise.d.ts.map +1 -0
- package/dist/sanitise.js +247 -0
- package/dist/sanitise.js.map +1 -0
- package/dist/scan.d.ts +60 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +246 -0
- package/dist/scan.js.map +1 -0
- package/dist/screens.d.ts +98 -0
- package/dist/screens.d.ts.map +1 -0
- package/dist/screens.js +301 -0
- package/dist/screens.js.map +1 -0
- package/dist/settle.d.ts +65 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +162 -0
- package/dist/settle.js.map +1 -0
- package/dist/shots.d.ts +109 -0
- package/dist/shots.d.ts.map +1 -0
- package/dist/shots.js +113 -0
- package/dist/shots.js.map +1 -0
- package/dist/style.d.ts +47 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +154 -0
- package/dist/style.js.map +1 -0
- package/dist/suite.d.ts +218 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +219 -0
- package/dist/suite.js.map +1 -0
- package/dist/summary.d.ts +268 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +1308 -0
- package/dist/summary.js.map +1 -0
- package/dist/unscannable.d.ts +75 -0
- package/dist/unscannable.d.ts.map +1 -0
- package/dist/unscannable.js +111 -0
- package/dist/unscannable.js.map +1 -0
- package/dist/versions.d.ts +13 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +39 -0
- package/dist/versions.js.map +1 -0
- package/dist/wcag-map.d.ts +19 -0
- package/dist/wcag-map.d.ts.map +1 -0
- package/dist/wcag-map.js +52 -0
- package/dist/wcag-map.js.map +1 -0
- package/dist/with-setsquare.d.ts +26 -0
- package/dist/with-setsquare.d.ts.map +1 -0
- package/dist/with-setsquare.js +70 -0
- package/dist/with-setsquare.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AxeFinding, RuleInapplicable, RulePass } from '@setsquare/schema';
|
|
2
|
+
import type { AxeRunResults } from './axe.js';
|
|
3
|
+
export declare function toFindings(results: AxeRunResults): AxeFinding[];
|
|
4
|
+
/**
|
|
5
|
+
* Rule-level pass rows.
|
|
6
|
+
*
|
|
7
|
+
* Emitted from the first release because P2 computes "regressed" as a JOIN
|
|
8
|
+
* against these, so a run ingested without them leaves a permanent hole in
|
|
9
|
+
* evidence history. `inapplicable` is deliberately excluded: a rule with nothing
|
|
10
|
+
* to check did not pass, and recording it as one would inflate every conformance
|
|
11
|
+
* claim built on this data.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toPasses(results: AxeRunResults): RulePass[];
|
|
14
|
+
/**
|
|
15
|
+
* EARL's `inapplicable`: rules that ran and found nothing to check.
|
|
16
|
+
*
|
|
17
|
+
* Rule-level like passes, but never conflated with them — a page with no
|
|
18
|
+
* `<video>` did not pass the captions rule. Carried because "your pages
|
|
19
|
+
* contain nothing this criterion regulates" is coverage a ledger must be able
|
|
20
|
+
* to state; without these rows it can only say "did not run", which
|
|
21
|
+
* understates what the scan did.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toInapplicable(results: AxeRunResults): RuleInapplicable[];
|
|
24
|
+
export declare function toWcag(tags: string[]): AxeFinding['wcag'] | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Bound and clean computed evidence.
|
|
27
|
+
*
|
|
28
|
+
* Three filters, narrowest first.
|
|
29
|
+
*
|
|
30
|
+
* **By name** (`isPermittedComputedValue`): a rule contributes the keys the
|
|
31
|
+
* contract says it may, and an unenumerated rule contributes numbers and
|
|
32
|
+
* booleans only. This is the filter that makes "no page text leaves the
|
|
33
|
+
* customer's CI" a property of the code rather than a habit of its authors — a
|
|
34
|
+
* check that starts putting `node.textContent` under a new key emits nothing
|
|
35
|
+
* until that key is declared in `@setsquare/schema`.
|
|
36
|
+
*
|
|
37
|
+
* **By shape**: nested objects and arrays are dropped, because that is where
|
|
38
|
+
* `relatedNodes` and other page fragments hide, and no rule family needs them
|
|
39
|
+
* for arithmetic.
|
|
40
|
+
*
|
|
41
|
+
* **By size**: strings are stripped of hostile characters and capped, the whole
|
|
42
|
+
* object is size-capped, and anything that will not serialise is dropped rather
|
|
43
|
+
* than risked. URL-valued keys lose their query and fragment first — a path is
|
|
44
|
+
* evidence, a query string is where tokens and addresses live.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sanitiseComputed(data: Record<string, unknown>, ruleId: string): Record<string, unknown> | undefined;
|
|
47
|
+
//# sourceMappingURL=findings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAIhF,OAAO,KAAK,EAA0B,aAAa,EAAE,MAAM,UAAU,CAAC;AAoCtE,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,UAAU,EAAE,CAK/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,QAAQ,EAAE,CAc3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,gBAAgB,EAAE,CASzE;AAiDD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,SAAS,CAkCrE;AAwCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAmBrC"}
|
package/dist/findings.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { isPermittedComputedValue, isUrlValuedComputedKey, stripUrlTail } from '@setsquare/schema';
|
|
2
|
+
import { MAX_RULE_ID_LENGTH } from '@setsquare/schema/limits';
|
|
3
|
+
import { capLength, sanitiseFailureSummary, sanitiseSelectors, stripHostileCharacters, } from './sanitise.js';
|
|
4
|
+
/**
|
|
5
|
+
* Map axe's output onto the contract's shapes.
|
|
6
|
+
*
|
|
7
|
+
* Everything crossing this boundary is untrusted: selectors and class names come
|
|
8
|
+
* from customer-page DOM, which routinely contains third-party embeds and
|
|
9
|
+
* user-generated content. Nothing is copied through without passing the
|
|
10
|
+
* sanitiser, and the two fields axe offers that we deliberately never carry are
|
|
11
|
+
* `node.html` (schema v1 has no snippet field, and this is the field that would
|
|
12
|
+
* otherwise smuggle whole page fragments into evidence) and `relatedNodes`
|
|
13
|
+
* (which contains the same HTML for other elements).
|
|
14
|
+
*/
|
|
15
|
+
/** Bounded, because computed evidence is page-derived like everything else. */
|
|
16
|
+
const MAX_COMPUTED_BYTES = 2048;
|
|
17
|
+
const IMPACTS = new Set(['minor', 'moderate', 'serious', 'critical']);
|
|
18
|
+
/**
|
|
19
|
+
* `wcag412` -> `4.1.2`, `wcag1410` -> `1.4.10`.
|
|
20
|
+
*
|
|
21
|
+
* The first two digits are always single, the rest is the criterion number —
|
|
22
|
+
* which is why this cannot be a naive split. 1.4.10 and 1.4.1 would be
|
|
23
|
+
* indistinguishable under one.
|
|
24
|
+
*/
|
|
25
|
+
const CRITERION_TAG = /^wcag(\d)(\d)(\d+)$/;
|
|
26
|
+
/** `wcag2a`, `wcag21aa`, `wcag22aa` — the version-and-level tags. */
|
|
27
|
+
const VERSION_TAG = /^wcag(\d)(\d)?(a{1,3})$/;
|
|
28
|
+
export function toFindings(results) {
|
|
29
|
+
return [
|
|
30
|
+
...results.violations.map((rule) => toFinding(rule, 'violation')),
|
|
31
|
+
...results.incomplete.map((rule) => toFinding(rule, 'incomplete')),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rule-level pass rows.
|
|
36
|
+
*
|
|
37
|
+
* Emitted from the first release because P2 computes "regressed" as a JOIN
|
|
38
|
+
* against these, so a run ingested without them leaves a permanent hole in
|
|
39
|
+
* evidence history. `inapplicable` is deliberately excluded: a rule with nothing
|
|
40
|
+
* to check did not pass, and recording it as one would inflate every conformance
|
|
41
|
+
* claim built on this data.
|
|
42
|
+
*/
|
|
43
|
+
export function toPasses(results) {
|
|
44
|
+
return results.passes.map((rule) => {
|
|
45
|
+
const pass = {
|
|
46
|
+
ruleId: capLength(stripHostileCharacters(rule.id), MAX_RULE_ID_LENGTH),
|
|
47
|
+
nodeCount: rule.nodes.length,
|
|
48
|
+
};
|
|
49
|
+
// The same WCAG identity findings carry, so the conformance projection can
|
|
50
|
+
// credit a passing rule to its criteria. Absent on best-practice rules,
|
|
51
|
+
// exactly as it is on findings — a pass row with no wcag block is advice
|
|
52
|
+
// that held, not conformance evidence.
|
|
53
|
+
const wcag = toWcag(rule.tags);
|
|
54
|
+
if (wcag)
|
|
55
|
+
pass.wcag = wcag;
|
|
56
|
+
return pass;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* EARL's `inapplicable`: rules that ran and found nothing to check.
|
|
61
|
+
*
|
|
62
|
+
* Rule-level like passes, but never conflated with them — a page with no
|
|
63
|
+
* `<video>` did not pass the captions rule. Carried because "your pages
|
|
64
|
+
* contain nothing this criterion regulates" is coverage a ledger must be able
|
|
65
|
+
* to state; without these rows it can only say "did not run", which
|
|
66
|
+
* understates what the scan did.
|
|
67
|
+
*/
|
|
68
|
+
export function toInapplicable(results) {
|
|
69
|
+
return results.inapplicable.map((rule) => {
|
|
70
|
+
const row = {
|
|
71
|
+
ruleId: capLength(stripHostileCharacters(rule.id), MAX_RULE_ID_LENGTH),
|
|
72
|
+
};
|
|
73
|
+
const wcag = toWcag(rule.tags);
|
|
74
|
+
if (wcag)
|
|
75
|
+
row.wcag = wcag;
|
|
76
|
+
return row;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function toFinding(rule, status) {
|
|
80
|
+
const selectors = rule.nodes.flatMap((node) => selectorsOf(node));
|
|
81
|
+
const targets = sanitiseSelectors(selectors);
|
|
82
|
+
const finding = {
|
|
83
|
+
ruleId: capLength(stripHostileCharacters(rule.id), MAX_RULE_ID_LENGTH),
|
|
84
|
+
status,
|
|
85
|
+
targets: {
|
|
86
|
+
// The TRUE total, even though `selectors` is capped. An agent needs to know
|
|
87
|
+
// the shape of the problem and its size; the cap only bounds the former.
|
|
88
|
+
nodeCount: rule.nodes.length,
|
|
89
|
+
...targets,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
// Axe reports no impact on some incompletes. Absent means unknown, and must
|
|
93
|
+
// not quietly become "minor".
|
|
94
|
+
if (rule.impact && IMPACTS.has(rule.impact)) {
|
|
95
|
+
finding.impact = rule.impact;
|
|
96
|
+
}
|
|
97
|
+
const wcag = toWcag(rule.tags);
|
|
98
|
+
if (wcag)
|
|
99
|
+
finding.wcag = wcag;
|
|
100
|
+
if (rule.helpUrl) {
|
|
101
|
+
// Axe appends `?application=axeAPI`, which is telemetry-ish noise on a link a
|
|
102
|
+
// human is meant to click.
|
|
103
|
+
const helpUrl = rule.helpUrl.split('?')[0];
|
|
104
|
+
if (helpUrl && helpUrl.startsWith('https://'))
|
|
105
|
+
finding.helpUrl = helpUrl;
|
|
106
|
+
}
|
|
107
|
+
const evidence = toEvidence(rule.nodes, finding.ruleId);
|
|
108
|
+
if (evidence)
|
|
109
|
+
finding.evidence = evidence;
|
|
110
|
+
return finding;
|
|
111
|
+
}
|
|
112
|
+
/** Axe's targets are strings, or arrays of strings for nested frames. */
|
|
113
|
+
function selectorsOf(node) {
|
|
114
|
+
return node.target.flatMap((entry) => {
|
|
115
|
+
if (typeof entry === 'string')
|
|
116
|
+
return [entry];
|
|
117
|
+
if (Array.isArray(entry))
|
|
118
|
+
return entry.filter((part) => typeof part === 'string');
|
|
119
|
+
return [];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function toWcag(tags) {
|
|
123
|
+
const criteria = [];
|
|
124
|
+
const versionTags = [];
|
|
125
|
+
let level;
|
|
126
|
+
for (const tag of tags) {
|
|
127
|
+
const criterion = CRITERION_TAG.exec(tag);
|
|
128
|
+
if (criterion) {
|
|
129
|
+
criteria.push(`${criterion[1]}.${criterion[2]}.${criterion[3]}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const version = VERSION_TAG.exec(tag);
|
|
133
|
+
if (version) {
|
|
134
|
+
versionTags.push(tag);
|
|
135
|
+
// Keep the STRONGEST level the rule carries. A rule tagged both wcag2a and
|
|
136
|
+
// wcag2aa is an A-level requirement; reporting AA would overstate what
|
|
137
|
+
// failing it means.
|
|
138
|
+
const candidate = version[3]?.toUpperCase();
|
|
139
|
+
if (!level || candidate.length < level.length)
|
|
140
|
+
level = candidate;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (criteria.length === 0 && versionTags.length === 0)
|
|
144
|
+
return undefined;
|
|
145
|
+
const wcag = {
|
|
146
|
+
// Sorted and deduplicated so two runs produce byte-identical evidence.
|
|
147
|
+
criteria: [...new Set(criteria)].sort().slice(0, 20),
|
|
148
|
+
versionTags: [...new Set(versionTags)].sort().slice(0, 20),
|
|
149
|
+
};
|
|
150
|
+
if (level)
|
|
151
|
+
wcag.level = level;
|
|
152
|
+
return wcag;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Failure summary plus computed evidence.
|
|
156
|
+
*
|
|
157
|
+
* `data` from the failing check is the "agent can do arithmetic" field — for
|
|
158
|
+
* contrast it holds the measured ratio, the required ratio and both colours,
|
|
159
|
+
* which is the difference between "fix the contrast" and "this token is 2.51:1
|
|
160
|
+
* and needs 4.5:1". Taken only from the check that failed, and only `data`:
|
|
161
|
+
* `relatedNodes` carries HTML for other elements and never crosses this line.
|
|
162
|
+
*/
|
|
163
|
+
function toEvidence(nodes, ruleId) {
|
|
164
|
+
const first = nodes[0];
|
|
165
|
+
if (!first)
|
|
166
|
+
return undefined;
|
|
167
|
+
const evidence = {};
|
|
168
|
+
const summary = sanitiseFailureSummary(first.failureSummary);
|
|
169
|
+
if (summary)
|
|
170
|
+
evidence.failureSummary = summary;
|
|
171
|
+
const computed = computedFrom(first, ruleId);
|
|
172
|
+
if (computed)
|
|
173
|
+
evidence.computed = computed;
|
|
174
|
+
return evidence.failureSummary || evidence.computed ? evidence : undefined;
|
|
175
|
+
}
|
|
176
|
+
function computedFrom(node, ruleId) {
|
|
177
|
+
for (const bucket of [node.any, node.all, node.none]) {
|
|
178
|
+
for (const check of bucket ?? []) {
|
|
179
|
+
const data = check.data;
|
|
180
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
181
|
+
continue;
|
|
182
|
+
const cleaned = sanitiseComputed(data, ruleId);
|
|
183
|
+
if (cleaned)
|
|
184
|
+
return cleaned;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Bound and clean computed evidence.
|
|
191
|
+
*
|
|
192
|
+
* Three filters, narrowest first.
|
|
193
|
+
*
|
|
194
|
+
* **By name** (`isPermittedComputedValue`): a rule contributes the keys the
|
|
195
|
+
* contract says it may, and an unenumerated rule contributes numbers and
|
|
196
|
+
* booleans only. This is the filter that makes "no page text leaves the
|
|
197
|
+
* customer's CI" a property of the code rather than a habit of its authors — a
|
|
198
|
+
* check that starts putting `node.textContent` under a new key emits nothing
|
|
199
|
+
* until that key is declared in `@setsquare/schema`.
|
|
200
|
+
*
|
|
201
|
+
* **By shape**: nested objects and arrays are dropped, because that is where
|
|
202
|
+
* `relatedNodes` and other page fragments hide, and no rule family needs them
|
|
203
|
+
* for arithmetic.
|
|
204
|
+
*
|
|
205
|
+
* **By size**: strings are stripped of hostile characters and capped, the whole
|
|
206
|
+
* object is size-capped, and anything that will not serialise is dropped rather
|
|
207
|
+
* than risked. URL-valued keys lose their query and fragment first — a path is
|
|
208
|
+
* evidence, a query string is where tokens and addresses live.
|
|
209
|
+
*/
|
|
210
|
+
export function sanitiseComputed(data, ruleId) {
|
|
211
|
+
const cleaned = {};
|
|
212
|
+
for (const [key, value] of Object.entries(data)) {
|
|
213
|
+
if (!isPermittedComputedValue(ruleId, key, value))
|
|
214
|
+
continue;
|
|
215
|
+
if (typeof value === 'string') {
|
|
216
|
+
const bounded = isUrlValuedComputedKey(ruleId, key) ? stripUrlTail(value) : value;
|
|
217
|
+
cleaned[stripHostileCharacters(key)] = capLength(stripHostileCharacters(bounded), 256);
|
|
218
|
+
}
|
|
219
|
+
else if (typeof value === 'number' || typeof value === 'boolean')
|
|
220
|
+
cleaned[stripHostileCharacters(key)] = value;
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(cleaned).length === 0)
|
|
223
|
+
return undefined;
|
|
224
|
+
const serialised = JSON.stringify(cleaned);
|
|
225
|
+
if (!serialised || serialised.length > MAX_COMPUTED_BYTES)
|
|
226
|
+
return undefined;
|
|
227
|
+
return cleaned;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=findings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findings.js","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BrowserContext, TestInfo } from '@playwright/test';
|
|
2
|
+
import type { ResolvedSetsquareOptions } from './options.js';
|
|
3
|
+
export declare function createContextFixture(options: ResolvedSetsquareOptions): ({ context, browserName }: {
|
|
4
|
+
context: BrowserContext;
|
|
5
|
+
browserName: string;
|
|
6
|
+
}, use: (context: BrowserContext) => Promise<void>, testInfo: TestInfo) => Promise<void>;
|
|
7
|
+
//# sourceMappingURL=fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAQ,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAYvE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAwE7D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,IAElE,0BAA0B;IAAE,OAAO,EAAE,cAAc,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAC1E,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,EAC/C,UAAU,QAAQ,KACjB,OAAO,CAAC,IAAI,CAAC,CAmFjB"}
|
package/dist/fixture.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { SCAN_RECORD_ATTACHMENT, SCAN_RECORD_VERSION, } from './contract.js';
|
|
2
|
+
import { flushAutoScan, installAutoScan } from './autoscan.js';
|
|
3
|
+
import { observePageRuntime } from './observe.js';
|
|
4
|
+
import { unscannableOmitted, unscannableRows } from './unscannable.js';
|
|
5
|
+
/**
|
|
6
|
+
* The capture half of the SDK: an override of Playwright's `context` fixture
|
|
7
|
+
* that watches every page and records what it saw.
|
|
8
|
+
*
|
|
9
|
+
* It forms no opinions. No dedupe, no inference, no aggregation, no disk — all of
|
|
10
|
+
* that is the reporter's, because the reporter is the only thing that exists once
|
|
11
|
+
* per run. This file runs in N worker processes, and anything stateful here is a
|
|
12
|
+
* cross-process bug in waiting.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Records SPA route changes from inside the page.
|
|
16
|
+
*
|
|
17
|
+
* `history.pushState` and `replaceState` fire no navigation event that Playwright
|
|
18
|
+
* can see, so a `framenavigated` listener alone observes ONE url on a
|
|
19
|
+
* single-page app and misses every route the user actually visited. Patching the
|
|
20
|
+
* two methods and listening for `popstate` is the only way to see them.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately writes to an array on `window` rather than exposing a binding or
|
|
23
|
+
* dispatching events: a binding installs a function the application can see, and
|
|
24
|
+
* this is the customer's page, not ours. The array is read back at teardown.
|
|
25
|
+
*
|
|
26
|
+
* KNOWN LIMIT, and it is why this is capture-only for now: `addInitScript` runs
|
|
27
|
+
* fresh on every real navigation, so the log resets with each document. Route
|
|
28
|
+
* changes made before the last full navigation are not recovered at teardown.
|
|
29
|
+
* That is fine while nothing depends on the timing, and it stops being fine the
|
|
30
|
+
* moment auto-scan needs to fire AT the route change rather than learn about it
|
|
31
|
+
* afterwards. That is the point at which this grows a binding (P1-4.2).
|
|
32
|
+
*/
|
|
33
|
+
const NAV_LOG_SCRIPT = `
|
|
34
|
+
(() => {
|
|
35
|
+
const log = [];
|
|
36
|
+
Object.defineProperty(window, '__setsquareNavLog', {
|
|
37
|
+
value: log,
|
|
38
|
+
// Non-enumerable so it does not show up in a customer's own iteration over
|
|
39
|
+
// window, and non-writable so page code cannot clobber it by accident.
|
|
40
|
+
enumerable: false,
|
|
41
|
+
writable: false,
|
|
42
|
+
configurable: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const record = (kind) => log.push({ kind, url: location.href, at: Date.now() });
|
|
46
|
+
|
|
47
|
+
for (const method of ['pushState', 'replaceState']) {
|
|
48
|
+
const original = history[method];
|
|
49
|
+
history[method] = function (...args) {
|
|
50
|
+
const result = original.apply(this, args);
|
|
51
|
+
try { record(method); } catch {}
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
window.addEventListener('popstate', () => { try { record('popstate'); } catch {} });
|
|
57
|
+
})();
|
|
58
|
+
`;
|
|
59
|
+
const NAV_KINDS = new Set(['pushState', 'replaceState', 'popstate']);
|
|
60
|
+
export function createContextFixture(options) {
|
|
61
|
+
return async function setsquareContext({ context, browserName }, use, testInfo) {
|
|
62
|
+
const startedAt = Date.now();
|
|
63
|
+
const observers = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
// Applies to pages that already exist and to every page opened later,
|
|
66
|
+
// including ones the test opens itself. A test driving two sessions is
|
|
67
|
+
// ordinary in a real suite, and attributing one page's evidence to another
|
|
68
|
+
// page's context would be worse than capturing nothing.
|
|
69
|
+
await context.addInitScript(NAV_LOG_SCRIPT).catch(() => {
|
|
70
|
+
// A context that refuses init scripts still yields real navigations below.
|
|
71
|
+
// Partial evidence beats none, provided we do not pretend it is complete.
|
|
72
|
+
});
|
|
73
|
+
const observe = (page) => {
|
|
74
|
+
const observer = { page, navigations: [] };
|
|
75
|
+
observers.push(observer);
|
|
76
|
+
/*
|
|
77
|
+
* What the page itself did: console errors, uncaught throws, failed
|
|
78
|
+
* requests, responses at or above 400.
|
|
79
|
+
*
|
|
80
|
+
* Attached here rather than at the context, so it covers pages the test
|
|
81
|
+
* opens itself and so a test that opens none pays nothing — which is the
|
|
82
|
+
* whole reason this is a `context` fixture and not a `page` one.
|
|
83
|
+
*/
|
|
84
|
+
observePageRuntime(page, startedAt);
|
|
85
|
+
page.on('framenavigated', (frame) => {
|
|
86
|
+
// Main frame only. An iframe navigating is not the user changing page,
|
|
87
|
+
// and treating it as one would file evidence against the wrong surface.
|
|
88
|
+
if (frame !== page.mainFrame())
|
|
89
|
+
return;
|
|
90
|
+
observer.navigations.push({
|
|
91
|
+
url: frame.url(),
|
|
92
|
+
atMs: Date.now() - startedAt,
|
|
93
|
+
kind: 'navigation',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
for (const page of context.pages())
|
|
98
|
+
observe(page);
|
|
99
|
+
context.on('page', observe);
|
|
100
|
+
if (options.autoScan && !options.disabled) {
|
|
101
|
+
// Installed before the test body runs, so the very first navigation is
|
|
102
|
+
// already being watched. A scanner that starts listening after the page has
|
|
103
|
+
// loaded has already missed the thing it exists to see.
|
|
104
|
+
await installAutoScan(context, options, testInfo, (message) => warnings.push(message)).catch((error) => {
|
|
105
|
+
warnings.push(`Automatic scanning could not be installed: ${error instanceof Error ? error.message : String(error)}. Explicit setsquare.scan() calls are unaffected.`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await use(context);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
// Teardown must never fail the test. The suite's verdict is the customer's,
|
|
113
|
+
// and our evidence-gathering has no standing to overturn it.
|
|
114
|
+
try {
|
|
115
|
+
// Before recording observations: whatever the test left on screen is the
|
|
116
|
+
// most reliable scan opportunity in the whole run, and for a fast test it
|
|
117
|
+
// is the only one.
|
|
118
|
+
if (options.autoScan && !options.disabled) {
|
|
119
|
+
await flushAutoScan(context, options, testInfo, (message) => warnings.push(message));
|
|
120
|
+
}
|
|
121
|
+
await attachRecords(observers, browserName, startedAt, testInfo);
|
|
122
|
+
for (const message of warnings) {
|
|
123
|
+
// Surfaced through Playwright's own annotations so a problem appears
|
|
124
|
+
// next to the test it happened in, not buried in a summary.
|
|
125
|
+
testInfo.annotations.push({ type: 'setsquare-warning', description: message });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Swallowed on purpose. A capture problem is reported through the run's
|
|
130
|
+
// warnings, not by reddening someone's suite.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function attachRecords(observers, browserName, startedAt, testInfo) {
|
|
136
|
+
for (const observer of observers) {
|
|
137
|
+
const observed = await observePage(observer, browserName, startedAt, testInfo.project.name);
|
|
138
|
+
if (!observed)
|
|
139
|
+
continue;
|
|
140
|
+
const record = {
|
|
141
|
+
recordVersion: SCAN_RECORD_VERSION,
|
|
142
|
+
kind: 'observation',
|
|
143
|
+
observed,
|
|
144
|
+
};
|
|
145
|
+
/*
|
|
146
|
+
* Surfaces this page reached and we could not scan.
|
|
147
|
+
*
|
|
148
|
+
* Rides the observation record because that is the one thing written per page
|
|
149
|
+
* at teardown, after every scan attempt has had its chance to fail. Absent
|
|
150
|
+
* when nothing was recorded — an empty array would be a claim that nothing
|
|
151
|
+
* went unscanned, which is a different statement.
|
|
152
|
+
*/
|
|
153
|
+
const unscanned = unscannableRows(observer.page);
|
|
154
|
+
if (unscanned) {
|
|
155
|
+
record.unscanned = unscanned;
|
|
156
|
+
const omitted = unscannableOmitted(observer.page);
|
|
157
|
+
if (omitted > 0)
|
|
158
|
+
record.unscannedOmitted = omitted;
|
|
159
|
+
}
|
|
160
|
+
await testInfo.attach(SCAN_RECORD_ATTACHMENT, {
|
|
161
|
+
body: Buffer.from(JSON.stringify(record), 'utf8'),
|
|
162
|
+
contentType: 'application/json',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function observePage(observer, browserName, startedAt, project) {
|
|
167
|
+
const { page } = observer;
|
|
168
|
+
const navigations = [...observer.navigations];
|
|
169
|
+
// A page closed by the test is not an error — plenty of specs close their own.
|
|
170
|
+
// Whatever was observed before it closed is still evidence.
|
|
171
|
+
if (!page.isClosed()) {
|
|
172
|
+
const routeChanges = await readRouteChanges(page, startedAt);
|
|
173
|
+
navigations.push(...routeChanges);
|
|
174
|
+
}
|
|
175
|
+
// `about:blank` is Playwright opening a context, not the suite visiting
|
|
176
|
+
// anything. Recording it would put a surface in the grid that no test asked for.
|
|
177
|
+
const meaningful = navigations.filter((nav) => nav.url && nav.url !== 'about:blank');
|
|
178
|
+
if (meaningful.length === 0)
|
|
179
|
+
return undefined;
|
|
180
|
+
meaningful.sort((a, b) => a.atMs - b.atMs);
|
|
181
|
+
const context = {
|
|
182
|
+
project,
|
|
183
|
+
engine: normaliseEngine(browserName),
|
|
184
|
+
navigations: meaningful,
|
|
185
|
+
};
|
|
186
|
+
if (!page.isClosed()) {
|
|
187
|
+
const viewport = page.viewportSize();
|
|
188
|
+
if (viewport)
|
|
189
|
+
context.viewport = viewport;
|
|
190
|
+
const theme = await readTheme(page);
|
|
191
|
+
if (theme)
|
|
192
|
+
context.observedTheme = theme;
|
|
193
|
+
}
|
|
194
|
+
return context;
|
|
195
|
+
}
|
|
196
|
+
async function readRouteChanges(page, startedAt) {
|
|
197
|
+
try {
|
|
198
|
+
const raw = await page.evaluate(() => window.__setsquareNavLog ?? []);
|
|
199
|
+
return raw
|
|
200
|
+
.filter((entry) => NAV_KINDS.has(entry.kind))
|
|
201
|
+
.map((entry) => ({
|
|
202
|
+
url: entry.url,
|
|
203
|
+
atMs: entry.at - startedAt,
|
|
204
|
+
kind: entry.kind,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Navigating away mid-read, a closed page, a CSP that blocked the init
|
|
209
|
+
// script. None of these is worth a warning on its own.
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function readTheme(page) {
|
|
214
|
+
try {
|
|
215
|
+
return await page.evaluate(() => {
|
|
216
|
+
const root = document.documentElement;
|
|
217
|
+
// Our own attribute first: a customer who has told us explicitly should not
|
|
218
|
+
// be second-guessed by a heuristic. `data-theme` is the widespread
|
|
219
|
+
// convention and is what the reference implementation uses.
|
|
220
|
+
return (root.getAttribute('data-setsquare-theme') ?? root.getAttribute('data-theme') ?? undefined);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Anything that is not one of Playwright's three engines is treated as chromium.
|
|
229
|
+
*
|
|
230
|
+
* Not a guess: `browserName` is a closed set in Playwright, and branded channels
|
|
231
|
+
* (`chrome`, `msedge`) are chromium. If a fourth engine ever appears this becomes
|
|
232
|
+
* wrong, which is what the compatibility matrix is for.
|
|
233
|
+
*/
|
|
234
|
+
function normaliseEngine(browserName) {
|
|
235
|
+
return browserName === 'firefox' || browserName === 'webkit' ? browserName : 'chromium';
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.js","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,GAIpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGvE;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;CAyBtB,CAAC;AAcF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,cAAc,EAAE,UAAU,CAAC,CAAC,CAAC;AAErE,MAAM,UAAU,oBAAoB,CAAC,OAAiC;IACpE,OAAO,KAAK,UAAU,gBAAgB,CACpC,EAAE,OAAO,EAAE,WAAW,EAAoD,EAC1E,GAA+C,EAC/C,QAAkB;QAElB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAmB,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAE9B,sEAAsE;QACtE,uEAAuE;QACvE,2EAA2E;QAC3E,wDAAwD;QACxD,MAAM,OAAO,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACrD,2EAA2E;YAC3E,0EAA0E;QAC5E,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,CAAC,IAAU,EAAQ,EAAE;YACnC,MAAM,QAAQ,GAAiB,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;YACzD,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAEzB;;;;;;;eAOG;YACH,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAEpC,IAAI,CAAC,EAAE,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE;gBAClC,uEAAuE;gBACvE,wEAAwE;gBACxE,IAAI,KAAK,KAAK,IAAI,CAAC,SAAS,EAAE;oBAAE,OAAO;gBACvC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC;oBACxB,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE;oBAChB,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;oBAC5B,IAAI,EAAE,YAAY;iBACnB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAClD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAE5B,IAAI,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC1C,uEAAuE;YACvE,4EAA4E;YAC5E,wDAAwD;YACxD,MAAM,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAC1F,CAAC,KAAc,EAAE,EAAE;gBACjB,QAAQ,CAAC,IAAI,CACX,8CACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,mDAAmD,CACpD,CAAC;YACJ,CAAC,CACF,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,4EAA4E;YAC5E,6DAA6D;YAC7D,IAAI,CAAC;gBACH,yEAAyE;gBACzE,0EAA0E;gBAC1E,mBAAmB;gBACnB,IAAI,OAAO,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;oBAC1C,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;gBACvF,CAAC;gBAED,MAAM,aAAa,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;gBACjE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;oBAC/B,qEAAqE;oBACrE,4DAA4D;oBAC5D,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;gBACjF,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wEAAwE;gBACxE,8CAA8C;YAChD,CAAC;QACH,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,SAAyB,EACzB,WAAmB,EACnB,SAAiB,EACjB,QAAkB;IAElB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5F,IAAI,CAAC,QAAQ;YAAE,SAAS;QAExB,MAAM,MAAM,GAAsB;YAChC,aAAa,EAAE,mBAAmB;YAClC,IAAI,EAAE,aAAa;YACnB,QAAQ;SACT,CAAC;QAEF;;;;;;;WAOG;QACH,MAAM,SAAS,GAAG,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;YAC7B,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAClD,IAAI,OAAO,GAAG,CAAC;gBAAE,MAAM,CAAC,gBAAgB,GAAG,OAAO,CAAC;QACrD,CAAC;QAED,MAAM,QAAQ,CAAC,MAAM,CAAC,sBAAsB,EAAE;YAC5C,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YACjD,WAAW,EAAE,kBAAkB;SAChC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,QAAsB,EACtB,WAAmB,EACnB,SAAiB,EACjB,OAAe;IAEf,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC;IAC1B,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAE9C,+EAA+E;IAC/E,4DAA4D;IAC5D,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QACrB,MAAM,YAAY,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC7D,WAAW,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;IACpC,CAAC;IAED,wEAAwE;IACxE,iFAAiF;IACjF,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC;IACrF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE9C,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAoB;QAC/B,OAAO;QACP,MAAM,EAAE,eAAe,CAAC,WAAW,CAAC;QACpC,WAAW,EAAE,UAAU;KACxB,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACrC,IAAI,QAAQ;YAAE,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAE1C,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK;YAAE,OAAO,CAAC,aAAa,GAAG,KAAK,CAAC;IAC3C,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,IAAU,EAAE,SAAiB;IAC3D,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAC7B,GAAG,EAAE,CAAE,MAA2D,CAAC,iBAAiB,IAAI,EAAE,CAC3F,CAAC;QAEF,OAAO,GAAG;aACP,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;aAC5C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACf,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,IAAI,EAAE,KAAK,CAAC,EAAE,GAAG,SAAS;YAC1B,IAAI,EAAE,KAAK,CAAC,IAAkC;SAC/C,CAAC,CAAC,CAAC;IACR,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,uDAAuD;QACvD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,IAAU;IACjC,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC;YACtC,4EAA4E;YAC5E,mEAAmE;YACnE,4DAA4D;YAC5D,OAAO,CACL,IAAI,CAAC,YAAY,CAAC,sBAAsB,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,SAAS,CAC1F,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,WAAmB;IAC1C,OAAO,WAAW,KAAK,SAAS,IAAI,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;AAC1F,CAAC"}
|