@brandlab-digital/conformance 3.7.0 → 3.8.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 (200) hide show
  1. package/README.md +26 -0
  2. package/dist/current-release.js +5 -5
  3. package/dist/current-release.js.map +1 -1
  4. package/dist/public-copy.d.ts +7 -0
  5. package/dist/public-copy.d.ts.map +1 -1
  6. package/dist/public-copy.js +8 -3
  7. package/dist/public-copy.js.map +1 -1
  8. package/dist/ready-cli.d.ts +38 -0
  9. package/dist/ready-cli.d.ts.map +1 -0
  10. package/dist/ready-cli.js +327 -0
  11. package/dist/ready-cli.js.map +1 -0
  12. package/dist/site-system/checks.d.ts +389 -0
  13. package/dist/site-system/checks.d.ts.map +1 -0
  14. package/dist/site-system/checks.js +96 -0
  15. package/dist/site-system/checks.js.map +1 -0
  16. package/dist/site-system/config.d.ts +40 -0
  17. package/dist/site-system/config.d.ts.map +1 -0
  18. package/dist/site-system/config.js +214 -0
  19. package/dist/site-system/config.js.map +1 -0
  20. package/dist/site-system/index.d.ts +14 -0
  21. package/dist/site-system/index.d.ts.map +1 -0
  22. package/dist/site-system/index.js +14 -0
  23. package/dist/site-system/index.js.map +1 -0
  24. package/dist/site-system/journeys/access.d.ts +7 -0
  25. package/dist/site-system/journeys/access.d.ts.map +1 -0
  26. package/dist/site-system/journeys/access.js +263 -0
  27. package/dist/site-system/journeys/access.js.map +1 -0
  28. package/dist/site-system/journeys/browser.d.ts +79 -0
  29. package/dist/site-system/journeys/browser.d.ts.map +1 -0
  30. package/dist/site-system/journeys/browser.js +218 -0
  31. package/dist/site-system/journeys/browser.js.map +1 -0
  32. package/dist/site-system/journeys/conversion.d.ts +3 -0
  33. package/dist/site-system/journeys/conversion.d.ts.map +1 -0
  34. package/dist/site-system/journeys/conversion.js +152 -0
  35. package/dist/site-system/journeys/conversion.js.map +1 -0
  36. package/dist/site-system/journeys/menu.d.ts +11 -0
  37. package/dist/site-system/journeys/menu.d.ts.map +1 -0
  38. package/dist/site-system/journeys/menu.js +395 -0
  39. package/dist/site-system/journeys/menu.js.map +1 -0
  40. package/dist/site-system/journeys/page-helpers.d.ts +146 -0
  41. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -0
  42. package/dist/site-system/journeys/page-helpers.js +426 -0
  43. package/dist/site-system/journeys/page-helpers.js.map +1 -0
  44. package/dist/site-system/journeys/plan.d.ts +82 -0
  45. package/dist/site-system/journeys/plan.d.ts.map +1 -0
  46. package/dist/site-system/journeys/plan.js +112 -0
  47. package/dist/site-system/journeys/plan.js.map +1 -0
  48. package/dist/site-system/journeys/shell.d.ts +24 -0
  49. package/dist/site-system/journeys/shell.d.ts.map +1 -0
  50. package/dist/site-system/journeys/shell.js +269 -0
  51. package/dist/site-system/journeys/shell.js.map +1 -0
  52. package/dist/site-system/playwright-report.d.ts +22 -0
  53. package/dist/site-system/playwright-report.d.ts.map +1 -0
  54. package/dist/site-system/playwright-report.js +131 -0
  55. package/dist/site-system/playwright-report.js.map +1 -0
  56. package/dist/site-system/records.d.ts +30 -0
  57. package/dist/site-system/records.d.ts.map +1 -0
  58. package/dist/site-system/records.js +98 -0
  59. package/dist/site-system/records.js.map +1 -0
  60. package/dist/site-system/report.d.ts +116 -0
  61. package/dist/site-system/report.d.ts.map +1 -0
  62. package/dist/site-system/report.js +366 -0
  63. package/dist/site-system/report.js.map +1 -0
  64. package/dist/site-system/rule-modules.d.ts +8 -0
  65. package/dist/site-system/rule-modules.d.ts.map +1 -0
  66. package/dist/site-system/rule-modules.js +12 -0
  67. package/dist/site-system/rule-modules.js.map +1 -0
  68. package/dist/site-system/rules/design/d4-distinctness.d.ts +36 -0
  69. package/dist/site-system/rules/design/d4-distinctness.d.ts.map +1 -0
  70. package/dist/site-system/rules/design/d4-distinctness.js +207 -0
  71. package/dist/site-system/rules/design/d4-distinctness.js.map +1 -0
  72. package/dist/site-system/rules/design/index.d.ts +17 -0
  73. package/dist/site-system/rules/design/index.d.ts.map +1 -0
  74. package/dist/site-system/rules/design/index.js +7 -0
  75. package/dist/site-system/rules/design/index.js.map +1 -0
  76. package/dist/site-system/rules/enquiry/c2-quote-form.d.ts +31 -0
  77. package/dist/site-system/rules/enquiry/c2-quote-form.d.ts.map +1 -0
  78. package/dist/site-system/rules/enquiry/c2-quote-form.js +211 -0
  79. package/dist/site-system/rules/enquiry/c2-quote-form.js.map +1 -0
  80. package/dist/site-system/rules/enquiry/common.d.ts +37 -0
  81. package/dist/site-system/rules/enquiry/common.d.ts.map +1 -0
  82. package/dist/site-system/rules/enquiry/common.js +59 -0
  83. package/dist/site-system/rules/enquiry/common.js.map +1 -0
  84. package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts +8 -0
  85. package/dist/site-system/rules/enquiry/e1-recorded-titles.d.ts.map +1 -0
  86. package/dist/site-system/rules/enquiry/e1-recorded-titles.js +59 -0
  87. package/dist/site-system/rules/enquiry/e1-recorded-titles.js.map +1 -0
  88. package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts +22 -0
  89. package/dist/site-system/rules/enquiry/e9-cost-guides.d.ts.map +1 -0
  90. package/dist/site-system/rules/enquiry/e9-cost-guides.js +55 -0
  91. package/dist/site-system/rules/enquiry/e9-cost-guides.js.map +1 -0
  92. package/dist/site-system/rules/enquiry/html.d.ts +53 -0
  93. package/dist/site-system/rules/enquiry/html.d.ts.map +1 -0
  94. package/dist/site-system/rules/enquiry/html.js +294 -0
  95. package/dist/site-system/rules/enquiry/html.js.map +1 -0
  96. package/dist/site-system/rules/enquiry/index.d.ts +17 -0
  97. package/dist/site-system/rules/enquiry/index.d.ts.map +1 -0
  98. package/dist/site-system/rules/enquiry/index.js +26 -0
  99. package/dist/site-system/rules/enquiry/index.js.map +1 -0
  100. package/dist/site-system/rules/enquiry/k8-sections.d.ts +17 -0
  101. package/dist/site-system/rules/enquiry/k8-sections.d.ts.map +1 -0
  102. package/dist/site-system/rules/enquiry/k8-sections.js +76 -0
  103. package/dist/site-system/rules/enquiry/k8-sections.js.map +1 -0
  104. package/dist/site-system/rules/enquiry/literal.d.ts +29 -0
  105. package/dist/site-system/rules/enquiry/literal.d.ts.map +1 -0
  106. package/dist/site-system/rules/enquiry/literal.js +189 -0
  107. package/dist/site-system/rules/enquiry/literal.js.map +1 -0
  108. package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts +73 -0
  109. package/dist/site-system/rules/enquiry/t11-licence-classes.d.ts.map +1 -0
  110. package/dist/site-system/rules/enquiry/t11-licence-classes.js +275 -0
  111. package/dist/site-system/rules/enquiry/t11-licence-classes.js.map +1 -0
  112. package/dist/site-system/rules/enquiry/t5-reviews.d.ts +23 -0
  113. package/dist/site-system/rules/enquiry/t5-reviews.d.ts.map +1 -0
  114. package/dist/site-system/rules/enquiry/t5-reviews.js +91 -0
  115. package/dist/site-system/rules/enquiry/t5-reviews.js.map +1 -0
  116. package/dist/site-system/rules/shell/banned-wording.d.ts +24 -0
  117. package/dist/site-system/rules/shell/banned-wording.d.ts.map +1 -0
  118. package/dist/site-system/rules/shell/banned-wording.js +212 -0
  119. package/dist/site-system/rules/shell/banned-wording.js.map +1 -0
  120. package/dist/site-system/rules/shell/budgets.d.ts +43 -0
  121. package/dist/site-system/rules/shell/budgets.d.ts.map +1 -0
  122. package/dist/site-system/rules/shell/budgets.js +297 -0
  123. package/dist/site-system/rules/shell/budgets.js.map +1 -0
  124. package/dist/site-system/rules/shell/cta-labels.d.ts +17 -0
  125. package/dist/site-system/rules/shell/cta-labels.d.ts.map +1 -0
  126. package/dist/site-system/rules/shell/cta-labels.js +86 -0
  127. package/dist/site-system/rules/shell/cta-labels.js.map +1 -0
  128. package/dist/site-system/rules/shell/facts.d.ts +33 -0
  129. package/dist/site-system/rules/shell/facts.d.ts.map +1 -0
  130. package/dist/site-system/rules/shell/facts.js +217 -0
  131. package/dist/site-system/rules/shell/facts.js.map +1 -0
  132. package/dist/site-system/rules/shell/headings.d.ts +5 -0
  133. package/dist/site-system/rules/shell/headings.d.ts.map +1 -0
  134. package/dist/site-system/rules/shell/headings.js +23 -0
  135. package/dist/site-system/rules/shell/headings.js.map +1 -0
  136. package/dist/site-system/rules/shell/hero-location.d.ts +4 -0
  137. package/dist/site-system/rules/shell/hero-location.d.ts.map +1 -0
  138. package/dist/site-system/rules/shell/hero-location.js +45 -0
  139. package/dist/site-system/rules/shell/hero-location.js.map +1 -0
  140. package/dist/site-system/rules/shell/html.d.ts +53 -0
  141. package/dist/site-system/rules/shell/html.d.ts.map +1 -0
  142. package/dist/site-system/rules/shell/html.js +341 -0
  143. package/dist/site-system/rules/shell/html.js.map +1 -0
  144. package/dist/site-system/rules/shell/index.d.ts +18 -0
  145. package/dist/site-system/rules/shell/index.d.ts.map +1 -0
  146. package/dist/site-system/rules/shell/index.js +65 -0
  147. package/dist/site-system/rules/shell/index.js.map +1 -0
  148. package/dist/site-system/rules/shell/licence.d.ts +4 -0
  149. package/dist/site-system/rules/shell/licence.d.ts.map +1 -0
  150. package/dist/site-system/rules/shell/licence.js +60 -0
  151. package/dist/site-system/rules/shell/licence.js.map +1 -0
  152. package/dist/site-system/rules/shell/location-wording.d.ts +13 -0
  153. package/dist/site-system/rules/shell/location-wording.d.ts.map +1 -0
  154. package/dist/site-system/rules/shell/location-wording.js +15 -0
  155. package/dist/site-system/rules/shell/location-wording.js.map +1 -0
  156. package/dist/site-system/rules/shell/navigation.d.ts +5 -0
  157. package/dist/site-system/rules/shell/navigation.d.ts.map +1 -0
  158. package/dist/site-system/rules/shell/navigation.js +272 -0
  159. package/dist/site-system/rules/shell/navigation.js.map +1 -0
  160. package/dist/site-system/rules/shell/overlays.d.ts +4 -0
  161. package/dist/site-system/rules/shell/overlays.d.ts.map +1 -0
  162. package/dist/site-system/rules/shell/overlays.js +93 -0
  163. package/dist/site-system/rules/shell/overlays.js.map +1 -0
  164. package/dist/site-system/rules/shell/page.d.ts +51 -0
  165. package/dist/site-system/rules/shell/page.d.ts.map +1 -0
  166. package/dist/site-system/rules/shell/page.js +256 -0
  167. package/dist/site-system/rules/shell/page.js.map +1 -0
  168. package/dist/site-system/rules/shell/phone.d.ts +13 -0
  169. package/dist/site-system/rules/shell/phone.d.ts.map +1 -0
  170. package/dist/site-system/rules/shell/phone.js +99 -0
  171. package/dist/site-system/rules/shell/phone.js.map +1 -0
  172. package/dist/site-system/rules/shell/proof-numbers.d.ts +7 -0
  173. package/dist/site-system/rules/shell/proof-numbers.d.ts.map +1 -0
  174. package/dist/site-system/rules/shell/proof-numbers.js +81 -0
  175. package/dist/site-system/rules/shell/proof-numbers.js.map +1 -0
  176. package/dist/site-system/rules/shell/types.d.ts +80 -0
  177. package/dist/site-system/rules/shell/types.d.ts.map +1 -0
  178. package/dist/site-system/rules/shell/types.js +17 -0
  179. package/dist/site-system/rules/shell/types.js.map +1 -0
  180. package/dist/site-system/runners.d.ts +37 -0
  181. package/dist/site-system/runners.d.ts.map +1 -0
  182. package/dist/site-system/runners.js +48 -0
  183. package/dist/site-system/runners.js.map +1 -0
  184. package/dist/site-system/samples.d.ts +43 -0
  185. package/dist/site-system/samples.d.ts.map +1 -0
  186. package/dist/site-system/samples.js +99 -0
  187. package/dist/site-system/samples.js.map +1 -0
  188. package/dist/site-system/steps.d.ts +66 -0
  189. package/dist/site-system/steps.d.ts.map +1 -0
  190. package/dist/site-system/steps.js +45 -0
  191. package/dist/site-system/steps.js.map +1 -0
  192. package/dist/site-system/streamed-html.d.ts +9 -0
  193. package/dist/site-system/streamed-html.d.ts.map +1 -0
  194. package/dist/site-system/streamed-html.js +43 -0
  195. package/dist/site-system/streamed-html.js.map +1 -0
  196. package/dist/site-system-e2e.d.ts +6 -0
  197. package/dist/site-system-e2e.d.ts.map +1 -0
  198. package/dist/site-system-e2e.js +54 -0
  199. package/dist/site-system-e2e.js.map +1 -0
  200. package/package.json +22 -5
@@ -0,0 +1,214 @@
1
+ /**
2
+ * What the ready gate reads from brandlab.config.ts: the Site System tier, the exceptions record and the
3
+ * representative routes. The file is parsed, never executed, so these fields must be written literally (as
4
+ * `defineBrandlabProject` requires anyway).
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import ts from "typescript";
9
+ import { SITE_SYSTEM_TIERS } from "./checks.js";
10
+ const CONFIG_FILE = "brandlab.config.ts";
11
+ const SITE_SYSTEM_KEYS = new Set(["tier", "newBuild", "samples", "labels", "freeQuotes", "thirdParties", "phones"]);
12
+ const HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
13
+ const TEL_HREF_PATTERN = /^tel:\+?\d{6,15}$/;
14
+ const NOT_LITERAL = Symbol("not literal");
15
+ export function readSiteSystemProjectConfig(root) {
16
+ const path = join(root, CONFIG_FILE);
17
+ if (!existsSync(path))
18
+ throw new Error(`${CONFIG_FILE} is missing, so the ready gate cannot tell which tier applies.`);
19
+ return parseSiteSystemProjectConfig(readFileSync(path, "utf8"));
20
+ }
21
+ /** Throws when siteSystem is present but unreadable or invalid: a declared tier must never be skipped silently. */
22
+ export function parseSiteSystemProjectConfig(text) {
23
+ const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
24
+ const config = findProjectObject(source);
25
+ if (!config) {
26
+ throw new Error(`${CONFIG_FILE} has no literal defineBrandlabProject({ ... }) call.`);
27
+ }
28
+ const problems = [];
29
+ const complete = config.properties.every(isPlainProperty);
30
+ if (!complete) {
31
+ problems.push(`defineBrandlabProject({ ... }) in ${CONFIG_FILE} spreads or computes a member, so the ready gate cannot read it in full; write every field literally.`);
32
+ }
33
+ const projectId = literal(property(config, "projectId"));
34
+ const siteSystemNode = property(config, "siteSystem");
35
+ const siteSystem = siteSystemNode === undefined ? null : readSiteSystem(literal(siteSystemNode));
36
+ const exceptionsNode = property(config, "exceptions");
37
+ let exceptions = [];
38
+ if (exceptionsNode !== undefined) {
39
+ const value = literal(exceptionsNode);
40
+ if (!Array.isArray(value)) {
41
+ problems.push(`The exceptions in ${CONFIG_FILE} are not written as a literal list, so none were applied.`);
42
+ }
43
+ else {
44
+ exceptions = value.flatMap((entry, index) => {
45
+ const exception = readException(entry);
46
+ if (!exception)
47
+ problems.push(`exceptions[${index}] in ${CONFIG_FILE} has no literal ruleId, rationale, evidence and reviewTrigger, so it was not applied.`);
48
+ return exception ? [exception] : [];
49
+ });
50
+ }
51
+ }
52
+ const searchCertification = literal(property(config, "searchCertification"));
53
+ const routes = isRecord(searchCertification) ? searchCertification.representativeRoutes : undefined;
54
+ const representativeRoutes = {};
55
+ if (isRecord(routes)) {
56
+ for (const [name, path] of Object.entries(routes)) {
57
+ if (typeof path === "string" && path.startsWith("/"))
58
+ representativeRoutes[name] = path;
59
+ }
60
+ }
61
+ else if (property(config, "searchCertification") !== undefined) {
62
+ problems.push(`searchCertification.representativeRoutes in ${CONFIG_FILE} is not literal, so only the home page was sampled from it.`);
63
+ }
64
+ return {
65
+ projectId: typeof projectId === "string" ? projectId : null,
66
+ siteSystem,
67
+ exceptions,
68
+ representativeRoutes,
69
+ complete,
70
+ problems,
71
+ };
72
+ }
73
+ /** `key: value` or `key` with an identifier or string key; spreads, computed keys and methods are not. */
74
+ function isPlainProperty(member) {
75
+ if (ts.isShorthandPropertyAssignment(member))
76
+ return !member.objectAssignmentInitializer;
77
+ return ts.isPropertyAssignment(member) && (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name));
78
+ }
79
+ function readSiteSystem(value) {
80
+ if (value === NOT_LITERAL || !isRecord(value)) {
81
+ throw new Error(`siteSystem in ${CONFIG_FILE} must be written as a literal object, for example siteSystem: { tier: 'shell' }.`);
82
+ }
83
+ const unknown = Object.keys(value).filter((key) => !SITE_SYSTEM_KEYS.has(key));
84
+ if (unknown.length > 0)
85
+ throw new Error(`siteSystem in ${CONFIG_FILE} contains unsupported keys: ${unknown.join(", ")}.`);
86
+ if (!SITE_SYSTEM_TIERS.includes(value.tier)) {
87
+ throw new Error(`siteSystem.tier in ${CONFIG_FILE} must be ${SITE_SYSTEM_TIERS.join(", ")}.`);
88
+ }
89
+ if (value.newBuild !== undefined && typeof value.newBuild !== "boolean") {
90
+ throw new Error(`siteSystem.newBuild in ${CONFIG_FILE} must be true or false.`);
91
+ }
92
+ if (value.samples !== undefined &&
93
+ (!Array.isArray(value.samples) || value.samples.some((path) => typeof path !== "string" || !path.startsWith("/")))) {
94
+ throw new Error(`siteSystem.samples in ${CONFIG_FILE} must be a literal list of root-relative paths.`);
95
+ }
96
+ if (value.freeQuotes !== undefined && typeof value.freeQuotes !== "boolean") {
97
+ throw new Error(`siteSystem.freeQuotes in ${CONFIG_FILE} must be true or false.`);
98
+ }
99
+ return {
100
+ tier: value.tier,
101
+ newBuild: value.newBuild === true,
102
+ samples: value.samples ?? [],
103
+ labels: stringList(value.labels, "labels", "non-empty labels", (label) => label.trim().length > 0),
104
+ freeQuotes: value.freeQuotes ?? null,
105
+ thirdParties: stringList(value.thirdParties, "thirdParties", "hostnames such as www.youtube.com", (host) => HOSTNAME_PATTERN.test(host)),
106
+ phones: stringList(value.phones, "phones", "tel: links such as tel:+61290001234", (href) => TEL_HREF_PATTERN.test(href)),
107
+ };
108
+ }
109
+ /** A literal list of unique strings that each pass `valid`, or [] when absent. */
110
+ function stringList(value, key, expected, valid) {
111
+ if (value === undefined)
112
+ return [];
113
+ if (!Array.isArray(value) ||
114
+ value.some((item) => typeof item !== "string" || !valid(item)) ||
115
+ new Set(value).size !== value.length) {
116
+ throw new Error(`siteSystem.${key} in ${CONFIG_FILE} must be a literal list of unique ${expected}.`);
117
+ }
118
+ return value;
119
+ }
120
+ function readException(value) {
121
+ if (!isRecord(value))
122
+ return null;
123
+ const { ruleId, rationale, evidence, expiresAt, reviewTrigger } = value;
124
+ if (typeof ruleId !== "string" || !ruleId.trim() ||
125
+ typeof rationale !== "string" ||
126
+ typeof evidence !== "string" ||
127
+ typeof reviewTrigger !== "string" ||
128
+ (expiresAt !== undefined && typeof expiresAt !== "string")) {
129
+ return null;
130
+ }
131
+ return { ruleId, rationale, evidence, reviewTrigger, ...(expiresAt !== undefined ? { expiresAt } : {}) };
132
+ }
133
+ function findProjectObject(source) {
134
+ let found;
135
+ const visit = (node) => {
136
+ if (found)
137
+ return;
138
+ if (ts.isCallExpression(node)) {
139
+ const callee = node.expression;
140
+ const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : null;
141
+ const argument = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;
142
+ if (name === "defineBrandlabProject" && argument && ts.isObjectLiteralExpression(argument)) {
143
+ found = argument;
144
+ return;
145
+ }
146
+ }
147
+ ts.forEachChild(node, visit);
148
+ };
149
+ visit(source);
150
+ return found;
151
+ }
152
+ function property(object, name) {
153
+ for (const member of object.properties) {
154
+ if (ts.isShorthandPropertyAssignment(member) && member.name.text === name)
155
+ return member.name;
156
+ if (!ts.isPropertyAssignment(member))
157
+ continue;
158
+ const key = member.name;
159
+ if ((ts.isIdentifier(key) || ts.isStringLiteralLike(key)) && key.text === name)
160
+ return member.initializer;
161
+ }
162
+ return undefined;
163
+ }
164
+ function unwrap(node) {
165
+ let current = node;
166
+ while (ts.isParenthesizedExpression(current) ||
167
+ ts.isAsExpression(current) ||
168
+ ts.isSatisfiesExpression(current) ||
169
+ ts.isNonNullExpression(current) ||
170
+ ts.isTypeAssertionExpression(current)) {
171
+ current = current.expression;
172
+ }
173
+ return current;
174
+ }
175
+ /** The JSON value of a literal expression, or NOT_LITERAL when any part is computed. */
176
+ function literal(node) {
177
+ if (!node)
178
+ return undefined;
179
+ const value = unwrap(node);
180
+ if (ts.isStringLiteralLike(value))
181
+ return value.text;
182
+ if (ts.isNumericLiteral(value))
183
+ return Number(value.text);
184
+ if (value.kind === ts.SyntaxKind.TrueKeyword)
185
+ return true;
186
+ if (value.kind === ts.SyntaxKind.FalseKeyword)
187
+ return false;
188
+ if (value.kind === ts.SyntaxKind.NullKeyword)
189
+ return null;
190
+ if (ts.isArrayLiteralExpression(value)) {
191
+ const items = value.elements.map((element) => (ts.isSpreadElement(element) ? NOT_LITERAL : literal(element)));
192
+ return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;
193
+ }
194
+ if (ts.isObjectLiteralExpression(value)) {
195
+ const object = {};
196
+ for (const member of value.properties) {
197
+ if (!ts.isPropertyAssignment(member))
198
+ return NOT_LITERAL;
199
+ const key = member.name;
200
+ if (!ts.isIdentifier(key) && !ts.isStringLiteralLike(key))
201
+ return NOT_LITERAL;
202
+ const item = literal(member.initializer);
203
+ if (item === NOT_LITERAL)
204
+ return NOT_LITERAL;
205
+ object[key.text] = item;
206
+ }
207
+ return object;
208
+ }
209
+ return NOT_LITERAL;
210
+ }
211
+ function isRecord(value) {
212
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
213
+ }
214
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the ready gate reads from brandlab.config.ts: the Site System tier, the exceptions record and the\n * representative routes. The file is parsed, never executed, so these fields must be written literally (as\n * `defineBrandlabProject` requires anyway).\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport ts from \"typescript\";\nimport { SITE_SYSTEM_TIERS, type SiteSystemTier } from \"./checks.js\";\n\nexport interface SiteSystemSettings {\n readonly tier: SiteSystemTier;\n /** A new build or redesign: D4 blocks whatever the tier. */\n readonly newBuild: boolean;\n /** Extra pages to sample for the rendered checks. */\n readonly samples: readonly string[];\n /** Quote and call-to-action labels recorded for this site, beyond the closed list (C5). */\n readonly labels: readonly string[];\n /** Whether the business offers free quotes; null when not recorded. */\n readonly freeQuotes: boolean | null;\n /** Third-party hostnames the site may load (B1). */\n readonly thirdParties: readonly string[];\n /** Extra tel: links the site legitimately shows (C10). */\n readonly phones: readonly string[];\n}\n\n/** A Romie-owned holdout; `ruleId` names the check it covers. */\nexport interface SiteSystemException {\n readonly ruleId: string;\n readonly rationale: string;\n readonly evidence: string;\n readonly expiresAt?: string;\n readonly reviewTrigger: string;\n}\n\nexport interface SiteSystemProjectConfig {\n readonly projectId: string | null;\n /** Null when the site has not adopted Site System v2: nothing in the gate blocks. */\n readonly siteSystem: SiteSystemSettings | null;\n readonly exceptions: readonly SiteSystemException[];\n /** `searchCertification.representativeRoutes` (home, contact, service, location, blog). */\n readonly representativeRoutes: Readonly<Record<string, string>>;\n /** False when defineBrandlabProject({ ... }) spreads or computes a member, so a tier could be hidden. */\n readonly complete: boolean;\n /** Fields that could not be read; the gate reports them and carries on without them. */\n readonly problems: readonly string[];\n}\n\nconst CONFIG_FILE = \"brandlab.config.ts\";\nconst SITE_SYSTEM_KEYS = new Set([\"tier\", \"newBuild\", \"samples\", \"labels\", \"freeQuotes\", \"thirdParties\", \"phones\"]);\nconst HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/;\nconst TEL_HREF_PATTERN = /^tel:\\+?\\d{6,15}$/;\nconst NOT_LITERAL = Symbol(\"not literal\");\n\nexport function readSiteSystemProjectConfig(root: string): SiteSystemProjectConfig {\n const path = join(root, CONFIG_FILE);\n if (!existsSync(path)) throw new Error(`${CONFIG_FILE} is missing, so the ready gate cannot tell which tier applies.`);\n return parseSiteSystemProjectConfig(readFileSync(path, \"utf8\"));\n}\n\n/** Throws when siteSystem is present but unreadable or invalid: a declared tier must never be skipped silently. */\nexport function parseSiteSystemProjectConfig(text: string): SiteSystemProjectConfig {\n const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);\n const config = findProjectObject(source);\n if (!config) {\n throw new Error(`${CONFIG_FILE} has no literal defineBrandlabProject({ ... }) call.`);\n }\n const problems: string[] = [];\n const complete = config.properties.every(isPlainProperty);\n if (!complete) {\n problems.push(`defineBrandlabProject({ ... }) in ${CONFIG_FILE} spreads or computes a member, so the ready gate cannot read it in full; write every field literally.`);\n }\n\n const projectId = literal(property(config, \"projectId\"));\n\n const siteSystemNode = property(config, \"siteSystem\");\n const siteSystem = siteSystemNode === undefined ? null : readSiteSystem(literal(siteSystemNode));\n\n const exceptionsNode = property(config, \"exceptions\");\n let exceptions: SiteSystemException[] = [];\n if (exceptionsNode !== undefined) {\n const value = literal(exceptionsNode);\n if (!Array.isArray(value)) {\n problems.push(`The exceptions in ${CONFIG_FILE} are not written as a literal list, so none were applied.`);\n } else {\n exceptions = value.flatMap((entry, index) => {\n const exception = readException(entry);\n if (!exception) problems.push(`exceptions[${index}] in ${CONFIG_FILE} has no literal ruleId, rationale, evidence and reviewTrigger, so it was not applied.`);\n return exception ? [exception] : [];\n });\n }\n }\n\n const searchCertification = literal(property(config, \"searchCertification\"));\n const routes = isRecord(searchCertification) ? searchCertification.representativeRoutes : undefined;\n const representativeRoutes: Record<string, string> = {};\n if (isRecord(routes)) {\n for (const [name, path] of Object.entries(routes)) {\n if (typeof path === \"string\" && path.startsWith(\"/\")) representativeRoutes[name] = path;\n }\n } else if (property(config, \"searchCertification\") !== undefined) {\n problems.push(`searchCertification.representativeRoutes in ${CONFIG_FILE} is not literal, so only the home page was sampled from it.`);\n }\n\n return {\n projectId: typeof projectId === \"string\" ? projectId : null,\n siteSystem,\n exceptions,\n representativeRoutes,\n complete,\n problems,\n };\n}\n\n/** `key: value` or `key` with an identifier or string key; spreads, computed keys and methods are not. */\nfunction isPlainProperty(member: ts.ObjectLiteralElementLike): boolean {\n if (ts.isShorthandPropertyAssignment(member)) return !member.objectAssignmentInitializer;\n return ts.isPropertyAssignment(member) && (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name));\n}\n\nfunction readSiteSystem(value: unknown): SiteSystemSettings {\n if (value === NOT_LITERAL || !isRecord(value)) {\n throw new Error(`siteSystem in ${CONFIG_FILE} must be written as a literal object, for example siteSystem: { tier: 'shell' }.`);\n }\n const unknown = Object.keys(value).filter((key) => !SITE_SYSTEM_KEYS.has(key));\n if (unknown.length > 0) throw new Error(`siteSystem in ${CONFIG_FILE} contains unsupported keys: ${unknown.join(\", \")}.`);\n if (!SITE_SYSTEM_TIERS.includes(value.tier as SiteSystemTier)) {\n throw new Error(`siteSystem.tier in ${CONFIG_FILE} must be ${SITE_SYSTEM_TIERS.join(\", \")}.`);\n }\n if (value.newBuild !== undefined && typeof value.newBuild !== \"boolean\") {\n throw new Error(`siteSystem.newBuild in ${CONFIG_FILE} must be true or false.`);\n }\n if (\n value.samples !== undefined &&\n (!Array.isArray(value.samples) || value.samples.some((path) => typeof path !== \"string\" || !path.startsWith(\"/\")))\n ) {\n throw new Error(`siteSystem.samples in ${CONFIG_FILE} must be a literal list of root-relative paths.`);\n }\n if (value.freeQuotes !== undefined && typeof value.freeQuotes !== \"boolean\") {\n throw new Error(`siteSystem.freeQuotes in ${CONFIG_FILE} must be true or false.`);\n }\n return {\n tier: value.tier as SiteSystemTier,\n newBuild: value.newBuild === true,\n samples: (value.samples as string[] | undefined) ?? [],\n labels: stringList(value.labels, \"labels\", \"non-empty labels\", (label) => label.trim().length > 0),\n freeQuotes: (value.freeQuotes as boolean | undefined) ?? null,\n thirdParties: stringList(value.thirdParties, \"thirdParties\", \"hostnames such as www.youtube.com\", (host) => HOSTNAME_PATTERN.test(host)),\n phones: stringList(value.phones, \"phones\", \"tel: links such as tel:+61290001234\", (href) => TEL_HREF_PATTERN.test(href)),\n };\n}\n\n/** A literal list of unique strings that each pass `valid`, or [] when absent. */\nfunction stringList(value: unknown, key: string, expected: string, valid: (item: string) => boolean): string[] {\n if (value === undefined) return [];\n if (\n !Array.isArray(value) ||\n value.some((item) => typeof item !== \"string\" || !valid(item)) ||\n new Set(value).size !== value.length\n ) {\n throw new Error(`siteSystem.${key} in ${CONFIG_FILE} must be a literal list of unique ${expected}.`);\n }\n return value as string[];\n}\n\nfunction readException(value: unknown): SiteSystemException | null {\n if (!isRecord(value)) return null;\n const { ruleId, rationale, evidence, expiresAt, reviewTrigger } = value;\n if (\n typeof ruleId !== \"string\" || !ruleId.trim() ||\n typeof rationale !== \"string\" ||\n typeof evidence !== \"string\" ||\n typeof reviewTrigger !== \"string\" ||\n (expiresAt !== undefined && typeof expiresAt !== \"string\")\n ) {\n return null;\n }\n return { ruleId, rationale, evidence, reviewTrigger, ...(expiresAt !== undefined ? { expiresAt } : {}) };\n}\n\nfunction findProjectObject(source: ts.SourceFile): ts.ObjectLiteralExpression | undefined {\n let found: ts.ObjectLiteralExpression | undefined;\n const visit = (node: ts.Node) => {\n if (found) return;\n if (ts.isCallExpression(node)) {\n const callee = node.expression;\n const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : null;\n const argument = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;\n if (name === \"defineBrandlabProject\" && argument && ts.isObjectLiteralExpression(argument)) {\n found = argument;\n return;\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(source);\n return found;\n}\n\nfunction property(object: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined {\n for (const member of object.properties) {\n if (ts.isShorthandPropertyAssignment(member) && member.name.text === name) return member.name;\n if (!ts.isPropertyAssignment(member)) continue;\n const key = member.name;\n if ((ts.isIdentifier(key) || ts.isStringLiteralLike(key)) && key.text === name) return member.initializer;\n }\n return undefined;\n}\n\nfunction unwrap(node: ts.Expression): ts.Expression {\n let current = node;\n while (\n ts.isParenthesizedExpression(current) ||\n ts.isAsExpression(current) ||\n ts.isSatisfiesExpression(current) ||\n ts.isNonNullExpression(current) ||\n ts.isTypeAssertionExpression(current)\n ) {\n current = current.expression;\n }\n return current;\n}\n\n/** The JSON value of a literal expression, or NOT_LITERAL when any part is computed. */\nfunction literal(node: ts.Expression | undefined): unknown {\n if (!node) return undefined;\n const value = unwrap(node);\n if (ts.isStringLiteralLike(value)) return value.text;\n if (ts.isNumericLiteral(value)) return Number(value.text);\n if (value.kind === ts.SyntaxKind.TrueKeyword) return true;\n if (value.kind === ts.SyntaxKind.FalseKeyword) return false;\n if (value.kind === ts.SyntaxKind.NullKeyword) return null;\n if (ts.isArrayLiteralExpression(value)) {\n const items = value.elements.map((element) => (ts.isSpreadElement(element) ? NOT_LITERAL : literal(element)));\n return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;\n }\n if (ts.isObjectLiteralExpression(value)) {\n const object: Record<string, unknown> = {};\n for (const member of value.properties) {\n if (!ts.isPropertyAssignment(member)) return NOT_LITERAL;\n const key = member.name;\n if (!ts.isIdentifier(key) && !ts.isStringLiteralLike(key)) return NOT_LITERAL;\n const item = literal(member.initializer);\n if (item === NOT_LITERAL) return NOT_LITERAL;\n object[key.text] = item;\n }\n return object;\n }\n return NOT_LITERAL;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The Site System v2 ready gate: the check registry, the rule-module interface `brandlab-ready` runs, the step
3
+ * records and the ready report.
4
+ */
5
+ export { SITE_SYSTEM_CHECKS, SITE_SYSTEM_TIERS, siteSystemCheckBlocks, type SiteSystemCheck, type SiteSystemCheckId, type SiteSystemCheckKind, type SiteSystemCheckScope, type SiteSystemFinding, type SiteSystemTier, } from "./checks.js";
6
+ export { parseSiteSystemProjectConfig, readSiteSystemProjectConfig, type SiteSystemException, type SiteSystemProjectConfig, type SiteSystemSettings, } from "./config.js";
7
+ export { readSiteSystemJourneyReport, siteSystemCheckIdsInTitle, type SiteSystemJourneyResults, } from "./playwright-report.js";
8
+ export { appendSiteSystemRecord, latestSiteSystemRecords, readSiteSystemRecords, SITE_SYSTEM_RECORD_PATHS, SITE_SYSTEM_RECORDS_SCHEMA, type SiteSystemRecord, type SiteSystemRecordsDocument, } from "./records.js";
9
+ export { buildSiteSystemReadyReport, isExceptionExpired, renderSiteSystemReadyMarkdown, SITE_SYSTEM_READY_REPORT_PATHS, SITE_SYSTEM_READY_SCHEMA, type BuildSiteSystemReadyReportInput, type SiteSystemReadyCheck, type SiteSystemReadyException, type SiteSystemReadyInputs, type SiteSystemReadyReport, type SiteSystemReadyStatus, type SiteSystemReadyStep, type SiteSystemReadyVerdict, type SiteSystemVerifyResult, } from "./report.js";
10
+ export { SITE_SYSTEM_RENDERED_RUNNERS } from "./rule-modules.js";
11
+ export { isSiteSystemFinding, runSiteSystemRenderedRunners, type SiteSystemRenderedInput, type SiteSystemRenderedRunner, } from "./runners.js";
12
+ export { builtPageFile, loadSiteSystemPages, planSiteSystemSamplePaths, resolveStreamedHtml, type SiteSystemFetch, type SiteSystemPage, type SiteSystemPageLoad, } from "./samples.js";
13
+ export { SITE_SYSTEM_HUMAN_STEPS, SITE_SYSTEM_LAUNCH_STEPS, SITE_SYSTEM_RECORDED_CHECK_IDS, siteSystemRecordFile, type SiteSystemHumanStepId, type SiteSystemLaunchStepId, type SiteSystemRecordFile, type SiteSystemStep, } from "./steps.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,8BAA8B,EAC9B,wBAAwB,EACxB,KAAK,+BAA+B,EACpC,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The Site System v2 ready gate: the check registry, the rule-module interface `brandlab-ready` runs, the step
3
+ * records and the ready report.
4
+ */
5
+ export { SITE_SYSTEM_CHECKS, SITE_SYSTEM_TIERS, siteSystemCheckBlocks, } from "./checks.js";
6
+ export { parseSiteSystemProjectConfig, readSiteSystemProjectConfig, } from "./config.js";
7
+ export { readSiteSystemJourneyReport, siteSystemCheckIdsInTitle, } from "./playwright-report.js";
8
+ export { appendSiteSystemRecord, latestSiteSystemRecords, readSiteSystemRecords, SITE_SYSTEM_RECORD_PATHS, SITE_SYSTEM_RECORDS_SCHEMA, } from "./records.js";
9
+ export { buildSiteSystemReadyReport, isExceptionExpired, renderSiteSystemReadyMarkdown, SITE_SYSTEM_READY_REPORT_PATHS, SITE_SYSTEM_READY_SCHEMA, } from "./report.js";
10
+ export { SITE_SYSTEM_RENDERED_RUNNERS } from "./rule-modules.js";
11
+ export { isSiteSystemFinding, runSiteSystemRenderedRunners, } from "./runners.js";
12
+ export { builtPageFile, loadSiteSystemPages, planSiteSystemSamplePaths, resolveStreamedHtml, } from "./samples.js";
13
+ export { SITE_SYSTEM_HUMAN_STEPS, SITE_SYSTEM_LAUNCH_STEPS, SITE_SYSTEM_RECORDED_CHECK_IDS, siteSystemRecordFile, } from "./steps.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,GAOtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,GAI5B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,GAE1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,GAG3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,8BAA8B,EAC9B,wBAAwB,GAUzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,4BAA4B,GAG7B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,GAIpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,oBAAoB,GAKrB,MAAM,YAAY,CAAC","sourcesContent":["/**\n * The Site System v2 ready gate: the check registry, the rule-module interface `brandlab-ready` runs, the step\n * records and the ready report.\n */\nexport {\n SITE_SYSTEM_CHECKS,\n SITE_SYSTEM_TIERS,\n siteSystemCheckBlocks,\n type SiteSystemCheck,\n type SiteSystemCheckId,\n type SiteSystemCheckKind,\n type SiteSystemCheckScope,\n type SiteSystemFinding,\n type SiteSystemTier,\n} from \"./checks.js\";\nexport {\n parseSiteSystemProjectConfig,\n readSiteSystemProjectConfig,\n type SiteSystemException,\n type SiteSystemProjectConfig,\n type SiteSystemSettings,\n} from \"./config.js\";\nexport {\n readSiteSystemJourneyReport,\n siteSystemCheckIdsInTitle,\n type SiteSystemJourneyResults,\n} from \"./playwright-report.js\";\nexport {\n appendSiteSystemRecord,\n latestSiteSystemRecords,\n readSiteSystemRecords,\n SITE_SYSTEM_RECORD_PATHS,\n SITE_SYSTEM_RECORDS_SCHEMA,\n type SiteSystemRecord,\n type SiteSystemRecordsDocument,\n} from \"./records.js\";\nexport {\n buildSiteSystemReadyReport,\n isExceptionExpired,\n renderSiteSystemReadyMarkdown,\n SITE_SYSTEM_READY_REPORT_PATHS,\n SITE_SYSTEM_READY_SCHEMA,\n type BuildSiteSystemReadyReportInput,\n type SiteSystemReadyCheck,\n type SiteSystemReadyException,\n type SiteSystemReadyInputs,\n type SiteSystemReadyReport,\n type SiteSystemReadyStatus,\n type SiteSystemReadyStep,\n type SiteSystemReadyVerdict,\n type SiteSystemVerifyResult,\n} from \"./report.js\";\nexport { SITE_SYSTEM_RENDERED_RUNNERS } from \"./rule-modules.js\";\nexport {\n isSiteSystemFinding,\n runSiteSystemRenderedRunners,\n type SiteSystemRenderedInput,\n type SiteSystemRenderedRunner,\n} from \"./runners.js\";\nexport {\n builtPageFile,\n loadSiteSystemPages,\n planSiteSystemSamplePaths,\n resolveStreamedHtml,\n type SiteSystemFetch,\n type SiteSystemPage,\n type SiteSystemPageLoad,\n} from \"./samples.js\";\nexport {\n SITE_SYSTEM_HUMAN_STEPS,\n SITE_SYSTEM_LAUNCH_STEPS,\n SITE_SYSTEM_RECORDED_CHECK_IDS,\n siteSystemRecordFile,\n type SiteSystemHumanStepId,\n type SiteSystemLaunchStepId,\n type SiteSystemRecordFile,\n type SiteSystemStep,\n} from \"./steps.js\";\n"]}
@@ -0,0 +1,7 @@
1
+ import { type SiteSystemJourneyPlan } from './plan.js';
2
+ /** The shortest side a primary action (call, quote, Menu) may have on a phone, with half a pixel for rounding. */
3
+ export declare const PRIMARY_TARGET_PX = 44;
4
+ export declare function defineAccessJourneys(plan: SiteSystemJourneyPlan, runs: ReadonlySet<string>, options: {
5
+ chromiumOnly: () => void;
6
+ }): void;
7
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../../src/site-system/journeys/access.ts"],"names":[],"mappings":"AAsBA,OAAO,EAA6F,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAGlJ,kHAAkH;AAClH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAgCpC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE;IAAE,YAAY,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,IAAI,CAoNxI"}
@@ -0,0 +1,263 @@
1
+ /**
2
+ * K3 to K6 in the browser. K3: text contrast (axe `color-contrast`: 4.5:1, 3:1 for large text) and the non-text
3
+ * contrast of icon-only controls (3:1) in the header, menu panels, drawer and footer. K4: primary actions (call, quote,
4
+ * Menu) at least 44 px on phones; every other target 24 px or spaced (axe `target-size`). K5: Tab through every
5
+ * focusable element at 390 px; none is ever fully hidden under the sticky header or the call bar (in every browser
6
+ * project the site runs). K6: the phone and quote actions in one order in the header, the hero and the call bar.
7
+ */
8
+ import { expect, test } from '@playwright/test';
9
+ import { describeAxe, mainNavigation, menuDisclosures, note, openSitePage, panelLocator, panelOpen, raiseTimeout, runAxe, siteFooter, siteHeader, waitUntil, } from './browser.js';
10
+ import { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_PHONE_VIEWPORT, siteSystemJourneyTitle as title } from './plan.js';
11
+ import { callBarLinks, menuButton, openDrawer } from './shell.js';
12
+ /** The shortest side a primary action (call, quote, Menu) may have on a phone, with half a pixel for rounding. */
13
+ export const PRIMARY_TARGET_PX = 44;
14
+ const SCOPE = Object.freeze({
15
+ header: '[data-brandlab-journey~="header"]',
16
+ nav: '[data-brandlab-journey~="nav"]',
17
+ panel: '[data-brandlab-journey~="panel"]',
18
+ drawer: '[data-brandlab-journey~="drawer"]',
19
+ footer: '[data-brandlab-journey~="footer"]',
20
+ callBar: '[data-brandlab-journey~="call-bar"]',
21
+ });
22
+ async function iconProblems(region, label) {
23
+ if (!(await region.count()))
24
+ return [];
25
+ const result = await region.evaluate((element) => window.__brandlabSiteSystem.iconContrastProblems(element));
26
+ return result.problems.map((problem) => `${label}: ${problem}`);
27
+ }
28
+ /** Open each desktop panel in turn and run `check` while it is open. */
29
+ async function withEachPanel(page, plan, check) {
30
+ const nav = await mainNavigation(page, plan);
31
+ if (!nav)
32
+ return;
33
+ for (const disclosure of await menuDisclosures(nav)) {
34
+ await disclosure.trigger.click();
35
+ if (!(await waitUntil(() => panelOpen(page, disclosure), 500)))
36
+ continue;
37
+ await page.waitForTimeout(250);
38
+ await panelLocator(page, disclosure.panelId).evaluate((panel) => window.__brandlabSiteSystem.markElement(panel, 'panel'));
39
+ await check(disclosure.label);
40
+ await disclosure.trigger.click();
41
+ await page.waitForTimeout(100);
42
+ }
43
+ }
44
+ export function defineAccessJourneys(plan, runs, options) {
45
+ const quote = { source: plan.quoteName.source, flags: plan.quoteName.flags };
46
+ test.describe('contrast, targets and order', () => {
47
+ options.chromiumOnly();
48
+ if (runs.has('K3')) {
49
+ test(title('K3', 'text and icons in the header, menu panels and footer meet contrast at 1440 px'), async ({ page }, testInfo) => {
50
+ await page.setViewportSize({ ...SITE_SYSTEM_DESKTOP_VIEWPORT });
51
+ await openSitePage(page, plan.home, plan);
52
+ const header = await siteHeader(page);
53
+ const footer = await siteFooter(page);
54
+ await mainNavigation(page, plan);
55
+ const problems = [];
56
+ const text = await runAxe(page, [SCOPE.header, SCOPE.nav, SCOPE.footer], { rules: ['color-contrast'] });
57
+ let unmeasured = text.incomplete;
58
+ problems.push(...describeAxe(text.violations));
59
+ problems.push(...(await iconProblems(header, 'header')), ...(await iconProblems(footer, 'footer')));
60
+ await withEachPanel(page, plan, async (label) => {
61
+ const open = await runAxe(page, [SCOPE.panel], { rules: ['color-contrast'] });
62
+ unmeasured += open.incomplete;
63
+ problems.push(...describeAxe(open.violations).map((line) => `${label} open: ${line}`));
64
+ problems.push(...(await iconProblems(page.locator(SCOPE.panel).filter({ visible: true }).first(), `${label} panel`)));
65
+ });
66
+ note(testInfo, `axe could not measure ${unmeasured} colour result(s) (text over images or overlapping layers); review them by eye`);
67
+ expect([...new Set(problems)]).toEqual([]);
68
+ });
69
+ test(title('K3', 'text and icons in the phone header, drawer and footer meet contrast at 390 px'), async ({ page }, testInfo) => {
70
+ await page.setViewportSize({ ...SITE_SYSTEM_PHONE_VIEWPORT });
71
+ await openSitePage(page, plan.home, plan);
72
+ const header = await siteHeader(page);
73
+ const footer = await siteFooter(page);
74
+ const problems = [];
75
+ const text = await runAxe(page, [SCOPE.header, SCOPE.footer], { rules: ['color-contrast'] });
76
+ let unmeasured = text.incomplete;
77
+ problems.push(...describeAxe(text.violations));
78
+ problems.push(...(await iconProblems(header, 'header')), ...(await iconProblems(footer, 'footer')));
79
+ const button = menuButton(header, plan);
80
+ if (await button.count()) {
81
+ const drawer = await openDrawer(page, button, 'click');
82
+ if (drawer) {
83
+ const group = drawer.locator('button[aria-expanded="false"], summary').filter({ visible: true }).first();
84
+ if (await group.count())
85
+ await group.click();
86
+ await page.waitForTimeout(150);
87
+ const inDrawer = await runAxe(page, [SCOPE.drawer], { rules: ['color-contrast'] });
88
+ unmeasured += inDrawer.incomplete;
89
+ problems.push(...describeAxe(inDrawer.violations).map((line) => `drawer: ${line}`));
90
+ problems.push(...(await iconProblems(drawer, 'drawer')));
91
+ }
92
+ }
93
+ note(testInfo, `axe could not measure ${unmeasured} colour result(s) (text over images or overlapping layers); review them by eye`);
94
+ expect([...new Set(problems)]).toEqual([]);
95
+ });
96
+ }
97
+ if (runs.has('K4')) {
98
+ test(title('K4', 'the call, quote and Menu actions are at least 44 px on phones'), async ({ page }, testInfo) => {
99
+ raiseTimeout(testInfo, 180_000);
100
+ await page.setViewportSize({ ...SITE_SYSTEM_PHONE_VIEWPORT });
101
+ const problems = [];
102
+ const measure = async (label, finder) => {
103
+ const targets = await page.evaluate(finder, quote);
104
+ for (const target of targets) {
105
+ if (target.width < PRIMARY_TARGET_PX - 0.5 || target.height < PRIMARY_TARGET_PX - 0.5) {
106
+ problems.push(`${label}: ${target.description} is ${Math.round(target.width)} x ${Math.round(target.height)} px (primary actions need 44 x 44)`);
107
+ }
108
+ }
109
+ return targets.length;
110
+ };
111
+ for (const path of [plan.home, ...plan.money]) {
112
+ await openSitePage(page, path, plan);
113
+ const header = await siteHeader(page);
114
+ await measure(`${path} header`, (name) => {
115
+ const helpers = window.__brandlabSiteSystem;
116
+ const root = document.querySelector('[data-brandlab-journey~="header"]');
117
+ if (!root)
118
+ return [];
119
+ const targets = [...Array.from(root.querySelectorAll('a[href^="tel:"]')).filter(helpers.isRendered), ...helpers.quoteActions(root, name)];
120
+ return targets.map((element) => ({ description: helpers.describeElement(element), ...element.getBoundingClientRect().toJSON() }));
121
+ });
122
+ const menu = menuButton(header, plan);
123
+ if (await menu.count()) {
124
+ const box = await menu.boundingBox();
125
+ if (box && (box.width < PRIMARY_TARGET_PX - 0.5 || box.height < PRIMARY_TARGET_PX - 0.5))
126
+ problems.push(`${path} header: the Menu button is ${Math.round(box.width)} x ${Math.round(box.height)} px (primary actions need 44 x 44)`);
127
+ }
128
+ await measure(`${path} hero`, (name) => {
129
+ const helpers = window.__brandlabSiteSystem;
130
+ const hero = helpers.heroRegion(name);
131
+ if (!hero)
132
+ return [];
133
+ const targets = [...Array.from(hero.querySelectorAll('a[href^="tel:"]')).filter(helpers.isRendered), ...helpers.quoteActions(hero, name)];
134
+ return targets.map((element) => ({ description: helpers.describeElement(element), ...element.getBoundingClientRect().toJSON() }));
135
+ });
136
+ if (path === plan.home && (await menu.count())) {
137
+ if (await openDrawer(page, menu, 'click')) {
138
+ await measure('drawer', (name) => {
139
+ const helpers = window.__brandlabSiteSystem;
140
+ const drawer = document.querySelector('[data-brandlab-journey~="drawer"]');
141
+ if (!drawer)
142
+ return [];
143
+ const targets = [...Array.from(drawer.querySelectorAll('a[href^="tel:"]')).filter(helpers.isRendered), ...helpers.quoteActions(drawer, name)];
144
+ return targets.map((element) => ({ description: helpers.describeElement(element), ...element.getBoundingClientRect().toJSON() }));
145
+ });
146
+ await page.keyboard.press('Escape');
147
+ await page.waitForTimeout(200);
148
+ }
149
+ }
150
+ if (await callBarLinks(page)) {
151
+ await measure(`${path} call bar`, (name) => {
152
+ const helpers = window.__brandlabSiteSystem;
153
+ const bar = document.querySelector('[data-brandlab-journey~="call-bar"]');
154
+ if (!bar)
155
+ return [];
156
+ const targets = [...Array.from(bar.querySelectorAll('a[href^="tel:"]')).filter(helpers.isRendered), ...helpers.quoteActions(bar, name)];
157
+ return targets.map((element) => ({ description: helpers.describeElement(element), ...element.getBoundingClientRect().toJSON() }));
158
+ });
159
+ }
160
+ }
161
+ expect([...new Set(problems)]).toEqual([]);
162
+ });
163
+ test(title('K4', 'every other target in the header, panels, drawer and footer is at least 24 px or spaced apart'), async ({ page }) => {
164
+ const problems = [];
165
+ await page.setViewportSize({ ...SITE_SYSTEM_DESKTOP_VIEWPORT });
166
+ await openSitePage(page, plan.home, plan);
167
+ await siteHeader(page);
168
+ await siteFooter(page);
169
+ await mainNavigation(page, plan);
170
+ problems.push(...describeAxe((await runAxe(page, [SCOPE.header, SCOPE.nav, SCOPE.footer], { rules: ['target-size'] })).violations).map((line) => `1440 px: ${line}`));
171
+ await withEachPanel(page, plan, async (label) => {
172
+ problems.push(...describeAxe((await runAxe(page, [SCOPE.panel], { rules: ['target-size'] })).violations).map((line) => `1440 px, ${label} open: ${line}`));
173
+ });
174
+ await page.setViewportSize({ ...SITE_SYSTEM_PHONE_VIEWPORT });
175
+ await openSitePage(page, plan.home, plan);
176
+ const header = await siteHeader(page);
177
+ await siteFooter(page);
178
+ problems.push(...describeAxe((await runAxe(page, [SCOPE.header, SCOPE.footer], { rules: ['target-size'] })).violations).map((line) => `390 px: ${line}`));
179
+ const menu = menuButton(header, plan);
180
+ if ((await menu.count()) && (await openDrawer(page, menu, 'click'))) {
181
+ problems.push(...describeAxe((await runAxe(page, [SCOPE.drawer], { rules: ['target-size'] })).violations).map((line) => `390 px drawer: ${line}`));
182
+ }
183
+ expect([...new Set(problems)]).toEqual([]);
184
+ });
185
+ }
186
+ if (runs.has('K6')) {
187
+ for (const path of plan.sampled) {
188
+ test(title('K6', `the phone and quote actions keep the ${plan.actionOrder} order in the header, hero and call bar on ${path}`), async ({ page }, testInfo) => {
189
+ raiseTimeout(testInfo, 180_000);
190
+ const problems = [];
191
+ const seen = [];
192
+ const record = (where, order) => {
193
+ if (order === 'incomplete')
194
+ return;
195
+ seen.push(`${where}: ${order}`);
196
+ if (order !== plan.actionOrder)
197
+ problems.push(`${where} shows the ${order === 'phone-first' ? 'phone before the quote' : 'quote before the phone'} (the site's order is ${plan.actionOrder})`);
198
+ };
199
+ for (const viewport of [SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_PHONE_VIEWPORT]) {
200
+ await page.setViewportSize({ ...viewport });
201
+ await openSitePage(page, path, plan);
202
+ const header = await siteHeader(page);
203
+ if (await header.count())
204
+ record(`${viewport.width} px header`, await header.evaluate((element, name) => window.__brandlabSiteSystem.actionOrder(element, name), quote));
205
+ record(`${viewport.width} px hero`, await page.evaluate((name) => {
206
+ const hero = window.__brandlabSiteSystem.heroRegion(name);
207
+ return hero ? window.__brandlabSiteSystem.actionOrder(hero, name) : 'incomplete';
208
+ }, quote));
209
+ if (viewport.width < 1024 && (await callBarLinks(page))) {
210
+ record(`${viewport.width} px call bar`, await page.locator(SCOPE.callBar).first().evaluate((element, name) => window.__brandlabSiteSystem.actionOrder(element, name), quote));
211
+ }
212
+ }
213
+ note(testInfo, seen.length ? `Orders seen: ${seen.join('; ')}` : 'No region shows both the phone and the quote action.');
214
+ if (path === plan.home && !seen.some((entry) => entry.startsWith(`${SITE_SYSTEM_DESKTOP_VIEWPORT.width} px`))) {
215
+ problems.push(`at ${SITE_SYSTEM_DESKTOP_VIEWPORT.width} px no region of the home page shows both the phone and the quote action, so their order cannot be checked`);
216
+ }
217
+ expect(problems).toEqual([]);
218
+ });
219
+ }
220
+ }
221
+ });
222
+ if (runs.has('K5')) {
223
+ test.describe('focus at 390 px', () => {
224
+ for (const path of plan.sampled) {
225
+ test(title('K5', `focus is never hidden under the sticky header or call bar on ${path} at 390 px`), async ({ page, browserName }, testInfo) => {
226
+ raiseTimeout(testInfo, 240_000);
227
+ await page.setViewportSize({ ...SITE_SYSTEM_PHONE_VIEWPORT });
228
+ await openSitePage(page, path, plan);
229
+ await siteHeader(page);
230
+ const reachable = await page.evaluate(() => window.__brandlabSiteSystem.tabbableCount());
231
+ const limit = Math.min(plan.maxTabStops, reachable + 10);
232
+ // WebKit on macOS moves focus to links only with Option held.
233
+ const key = browserName === 'webkit' && process.platform === 'darwin' ? 'Alt+Tab' : 'Tab';
234
+ const hidden = [];
235
+ let stops = 0;
236
+ let outside = 0;
237
+ for (let press = 0; press < limit; press += 1) {
238
+ await page.keyboard.press(key);
239
+ // Measure one frame later: a bar that hides on its observer's next frame is not caught mid-transition.
240
+ const step = await page.evaluate(() => window.__brandlabSiteSystem.focusStepAfterFrame());
241
+ if (step.repeat)
242
+ break;
243
+ if (step.done) {
244
+ outside += 1;
245
+ if (outside >= 2)
246
+ break;
247
+ continue;
248
+ }
249
+ stops += 1;
250
+ if (step.obscured)
251
+ hidden.push(`${step.obscured} is hidden under the ${step.covered}`);
252
+ }
253
+ if (stops >= plan.maxTabStops)
254
+ note(testInfo, `${path}: stopped after ${plan.maxTabStops} Tab presses (about ${reachable} focusable elements); raise maxTabStops to walk the rest`);
255
+ note(testInfo, `${path}: ${stops} focus stop(s) checked in ${browserName}`);
256
+ expect(stops, `${path}: Tab reached no element`).toBeGreaterThan(0);
257
+ expect(hidden).toEqual([]);
258
+ });
259
+ }
260
+ });
261
+ }
262
+ }
263
+ //# sourceMappingURL=access.js.map