@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,81 @@
1
+ import { closest, collapseWhitespace } from "./html.js";
2
+ import { elementAt } from "./page.js";
3
+ import { failFinding, passFinding, sampledPages } from "./types.js";
4
+ /**
5
+ * T4: no hard-coded rating or review numbers. A number that reads as a rating ("4.9 stars", "5.0 rating", "4.8 out of
6
+ * 5") or a review count ("from 132 reviews") has to sit inside an element the Google feed renders: a `data-bl="stars"`
7
+ * element, the `summary` slot of the reviews section, a `data-bl-source="feed"` marker, or a customer's own review
8
+ * text. Anything else is typed by hand.
9
+ */
10
+ /** Words that, straight after "out of 5", show the number counts something other than a rating ("3 out of 5 leaks"). */
11
+ const NOT_AFTER_OUT_OF_FIVE = "(?!\\s+(?!(?:stars?|reviews?|ratings?|google|customers?|clients?|homeowners?|people|users)\\b)[a-z]{3,}s\\b)";
12
+ /** What may follow "rated 4" or "rating of 4.9": the end of the phrase, "out of 5", "stars" or a place it was rated. */
13
+ const AFTER_RATED_NUMBER = "(?=\\s*(?:$|[.,;:!?)\\]]|\\/\\s*5|out\\b|stars?\\b|on\\b|from\\b|by\\b|at\\b|across\\b|with\\b|and\\b|in\\b))";
14
+ const RATING_PATTERNS = [
15
+ new RegExp(`(?<![\\d.])[1-5](?:\\.\\d)?\\s*(?:\\/\\s*5|out\\s+of\\s+5)\\b${NOT_AFTER_OUT_OF_FIVE}`, "gi"),
16
+ /(?<![\d.])[1-5](?:\.\d)?[\s-]*(?:stars?|star[- ]rat(?:ed|ing))\b/gi,
17
+ /(?<![\d.])[1-5](?:\.\d)?\s*(?:rating|rated)\b/gi,
18
+ new RegExp(`\\b(?:rated|rating(?:\\s+of)?|scores?|scored)\\s*[:-]?\\s*[1-5](?:\\.\\d)?\\b${AFTER_RATED_NUMBER}`, "gi"),
19
+ /(?<![\d,.$])\d{1,3}(?:,\d{3})*\+?\s+(?:(?:five|5)[- ]star\s+)?(?:(?:google|verified|customer|client)\s+)*(?:reviews?|ratings?)\b/gi,
20
+ /[\u2605\u2b50]\s*[1-5](?:\.\d)?\b|\b[1-5](?:\.\d)?\s*[\u2605\u2b50]/gu,
21
+ ];
22
+ /** Star ratings that rate a product or a supply, not the business: energy and water labels, phases, solar. */
23
+ const NOT_A_REVIEW_RATING = /\b(?:energy|wels|nathers|water[- ]efficien\w*|appliances?|phase|solar|inverters?|kw|kva)\b/i;
24
+ const REVIEW_WORDS = /\b(?:google|reviews?|customers?|clients?|testimonials?)\b/i;
25
+ const CUSTOMER_TEXT_SLOTS = new Set(["list", "item", "review", "text", "body", "quote", "author"]);
26
+ /** Whether the element is rendered from the feed, or is a customer's own words. */
27
+ export function isFeedRendered(element) {
28
+ return closest(element, (candidate) => {
29
+ if (candidate.attrs["data-bl"] === "stars" || candidate.attrs["data-bl-source"] === "feed")
30
+ return true;
31
+ if (candidate.tag === "blockquote" || candidate.attrs.itemprop === "reviewBody")
32
+ return true;
33
+ const slot = candidate.attrs["data-slot"];
34
+ if (slot === undefined)
35
+ return false;
36
+ const inReviews = closest(candidate, (ancestor) => ancestor.attrs["data-bl-section"] === "reviews") !== null;
37
+ return inReviews && (slot === "summary" || CUSTOMER_TEXT_SLOTS.has(slot));
38
+ }) !== null;
39
+ }
40
+ /** The stretches of text that read as a rating or review count, overlapping matches merged. */
41
+ function ratingRanges(text) {
42
+ const ranges = RATING_PATTERNS.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length })))
43
+ .sort((left, right) => left.start - right.start || right.end - left.end);
44
+ const merged = [];
45
+ const counts = (range) => {
46
+ const own = text.slice(range.start, range.end);
47
+ if (REVIEW_WORDS.test(own))
48
+ return true;
49
+ return !NOT_A_REVIEW_RATING.test(text.slice(Math.max(0, range.start - 30), range.end + 30));
50
+ };
51
+ for (const range of ranges) {
52
+ const last = merged[merged.length - 1];
53
+ if (last && range.start <= last.end)
54
+ last.end = Math.max(last.end, range.end);
55
+ else
56
+ merged.push({ ...range });
57
+ }
58
+ return merged.filter(counts);
59
+ }
60
+ export function checkProofNumbers(pages) {
61
+ const findings = [];
62
+ for (const page of pages) {
63
+ const quotes = new Set();
64
+ for (const unit of [...page.units, ...page.attributeCopy]) {
65
+ for (const range of ratingRanges(unit.text)) {
66
+ if (!isFeedRendered(elementAt(unit, range.start)))
67
+ quotes.add(collapseWhitespace(unit.text.slice(range.start, range.end)));
68
+ }
69
+ }
70
+ for (const text of page.metadata) {
71
+ for (const range of ratingRanges(text))
72
+ quotes.add(collapseWhitespace(text.slice(range.start, range.end)));
73
+ }
74
+ if (quotes.size > 0) {
75
+ const shown = [...quotes].slice(0, 3).map((quote) => `"${quote}"`).join(", ");
76
+ findings.push(failFinding("T4", page.path, `The page ${page.path} shows a rating or review number typed by hand (${shown}) outside the Google feed.`));
77
+ }
78
+ }
79
+ return findings.length > 0 ? findings : [passFinding("T4", `No hard-coded rating or review numbers on ${sampledPages(pages.length)}; every one comes from the feed.`)];
80
+ }
81
+ //# sourceMappingURL=proof-numbers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-numbers.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/proof-numbers.ts"],"names":[],"mappings":"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","sourcesContent":["import { closest, collapseWhitespace, type HtmlElement } from \"./html.js\";\nimport { elementAt, type ShellPage } from \"./page.js\";\nimport { failFinding, passFinding, sampledPages, type ShellFinding } from \"./types.js\";\n\n/**\n * T4: no hard-coded rating or review numbers. A number that reads as a rating (\"4.9 stars\", \"5.0 rating\", \"4.8 out of\n * 5\") or a review count (\"from 132 reviews\") has to sit inside an element the Google feed renders: a `data-bl=\"stars\"`\n * element, the `summary` slot of the reviews section, a `data-bl-source=\"feed\"` marker, or a customer's own review\n * text. Anything else is typed by hand.\n */\n\n/** Words that, straight after \"out of 5\", show the number counts something other than a rating (\"3 out of 5 leaks\"). */\nconst NOT_AFTER_OUT_OF_FIVE = \"(?!\\\\s+(?!(?:stars?|reviews?|ratings?|google|customers?|clients?|homeowners?|people|users)\\\\b)[a-z]{3,}s\\\\b)\";\n/** What may follow \"rated 4\" or \"rating of 4.9\": the end of the phrase, \"out of 5\", \"stars\" or a place it was rated. */\nconst AFTER_RATED_NUMBER = \"(?=\\\\s*(?:$|[.,;:!?)\\\\]]|\\\\/\\\\s*5|out\\\\b|stars?\\\\b|on\\\\b|from\\\\b|by\\\\b|at\\\\b|across\\\\b|with\\\\b|and\\\\b|in\\\\b))\";\n\nconst RATING_PATTERNS: readonly RegExp[] = [\n new RegExp(`(?<![\\\\d.])[1-5](?:\\\\.\\\\d)?\\\\s*(?:\\\\/\\\\s*5|out\\\\s+of\\\\s+5)\\\\b${NOT_AFTER_OUT_OF_FIVE}`, \"gi\"),\n /(?<![\\d.])[1-5](?:\\.\\d)?[\\s-]*(?:stars?|star[- ]rat(?:ed|ing))\\b/gi,\n /(?<![\\d.])[1-5](?:\\.\\d)?\\s*(?:rating|rated)\\b/gi,\n new RegExp(`\\\\b(?:rated|rating(?:\\\\s+of)?|scores?|scored)\\\\s*[:-]?\\\\s*[1-5](?:\\\\.\\\\d)?\\\\b${AFTER_RATED_NUMBER}`, \"gi\"),\n /(?<![\\d,.$])\\d{1,3}(?:,\\d{3})*\\+?\\s+(?:(?:five|5)[- ]star\\s+)?(?:(?:google|verified|customer|client)\\s+)*(?:reviews?|ratings?)\\b/gi,\n /[\\u2605\\u2b50]\\s*[1-5](?:\\.\\d)?\\b|\\b[1-5](?:\\.\\d)?\\s*[\\u2605\\u2b50]/gu,\n];\n\n/** Star ratings that rate a product or a supply, not the business: energy and water labels, phases, solar. */\nconst NOT_A_REVIEW_RATING = /\\b(?:energy|wels|nathers|water[- ]efficien\\w*|appliances?|phase|solar|inverters?|kw|kva)\\b/i;\nconst REVIEW_WORDS = /\\b(?:google|reviews?|customers?|clients?|testimonials?)\\b/i;\n\nconst CUSTOMER_TEXT_SLOTS = new Set([\"list\", \"item\", \"review\", \"text\", \"body\", \"quote\", \"author\"]);\n\n/** Whether the element is rendered from the feed, or is a customer's own words. */\nexport function isFeedRendered(element: HtmlElement): boolean {\n return closest(element, (candidate) => {\n if (candidate.attrs[\"data-bl\"] === \"stars\" || candidate.attrs[\"data-bl-source\"] === \"feed\") return true;\n if (candidate.tag === \"blockquote\" || candidate.attrs.itemprop === \"reviewBody\") return true;\n const slot = candidate.attrs[\"data-slot\"];\n if (slot === undefined) return false;\n const inReviews = closest(candidate, (ancestor) => ancestor.attrs[\"data-bl-section\"] === \"reviews\") !== null;\n return inReviews && (slot === \"summary\" || CUSTOMER_TEXT_SLOTS.has(slot));\n }) !== null;\n}\n\n/** The stretches of text that read as a rating or review count, overlapping matches merged. */\nfunction ratingRanges(text: string): { start: number; end: number }[] {\n const ranges = RATING_PATTERNS.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length })))\n .sort((left, right) => left.start - right.start || right.end - left.end);\n const merged: { start: number; end: number }[] = [];\n const counts = (range: { start: number; end: number }) => {\n const own = text.slice(range.start, range.end);\n if (REVIEW_WORDS.test(own)) return true;\n return !NOT_A_REVIEW_RATING.test(text.slice(Math.max(0, range.start - 30), range.end + 30));\n };\n for (const range of ranges) {\n const last = merged[merged.length - 1];\n if (last && range.start <= last.end) last.end = Math.max(last.end, range.end);\n else merged.push({ ...range });\n }\n return merged.filter(counts);\n}\n\nexport function checkProofNumbers(pages: readonly ShellPage[]): ShellFinding[] {\n const findings: ShellFinding[] = [];\n for (const page of pages) {\n const quotes = new Set<string>();\n for (const unit of [...page.units, ...page.attributeCopy]) {\n for (const range of ratingRanges(unit.text)) {\n if (!isFeedRendered(elementAt(unit, range.start))) quotes.add(collapseWhitespace(unit.text.slice(range.start, range.end)));\n }\n }\n for (const text of page.metadata) {\n for (const range of ratingRanges(text)) quotes.add(collapseWhitespace(text.slice(range.start, range.end)));\n }\n if (quotes.size > 0) {\n const shown = [...quotes].slice(0, 3).map((quote) => `\"${quote}\"`).join(\", \");\n findings.push(failFinding(\"T4\", page.path, `The page ${page.path} shows a rating or review number typed by hand (${shown}) outside the Google feed.`));\n }\n }\n return findings.length > 0 ? findings : [passFinding(\"T4\", `No hard-coded rating or review numbers on ${sampledPages(pages.length)}; every one comes from the feed.`)];\n}\n"]}
@@ -0,0 +1,80 @@
1
+ import type { SiteSystemCheckId, SiteSystemFinding } from "../../checks.js";
2
+ import type { SiteSystemSettings } from "../../config.js";
3
+ /** What a rendered runner receives: the sampled pages' server HTML and where the project lives. */
4
+ export interface ShellRenderedInput {
5
+ /** The origin the pages were fetched from; first-party means this host. */
6
+ baseUrl: string;
7
+ pages: readonly {
8
+ path: string;
9
+ html: string;
10
+ }[];
11
+ projectRoot: string;
12
+ /** The gate's parsed `siteSystem` settings (null when not declared); read from brandlab.config.ts when absent. */
13
+ siteSystem?: SiteSystemSettings | null;
14
+ }
15
+ /** The runner shape `brandlab-ready` calls (the same as the gate's `SiteSystemRenderedRunner`). */
16
+ export type ShellRenderedRunner = (input: ShellRenderedInput) => Promise<readonly SiteSystemFinding[]>;
17
+ /**
18
+ * A finding, optionally marked as a warning. A warning is reported with status "pass" because the finding type has
19
+ * no warning status: it advises and never blocks.
20
+ */
21
+ export interface ShellFinding extends SiteSystemFinding {
22
+ readonly severity?: "warning";
23
+ }
24
+ export interface ShellLicenceFact {
25
+ name: string;
26
+ identifier: string;
27
+ jurisdiction?: string;
28
+ /** The licensee, when the licence record names one that differs from the trading name. */
29
+ holder?: string;
30
+ /** ISO dates, judged by site-core's licenceRenderState: an expired licence never renders. */
31
+ expiresOn?: string;
32
+ checkedOn?: string;
33
+ }
34
+ /** The parts of the project's business facts the shell rules compare the pages against. */
35
+ export interface ShellProjectFacts {
36
+ tradingName?: string;
37
+ phone?: {
38
+ display: string;
39
+ e164: string;
40
+ telHref: string;
41
+ };
42
+ licences: readonly ShellLicenceFact[];
43
+ /** Why the licence data could not be read, when it exists but is not plain data: T3 then does not run. */
44
+ licencesUnreadable?: string;
45
+ /** Project-relative files the facts were read from. */
46
+ sources: readonly string[];
47
+ }
48
+ /** Reads the bytes behind a URL the page references: a script, a stylesheet or the hero image. */
49
+ export type ShellAssetReader = (url: URL, context: {
50
+ projectRoot: string;
51
+ baseUrl: string;
52
+ }) => Promise<Uint8Array | undefined>;
53
+ export interface ShellRuleOptions {
54
+ /** The business facts, when the caller has them; otherwise they are read from `src/data` in the project. */
55
+ facts?: ShellProjectFacts | null;
56
+ /** Test override of `siteSystem.labels`: quote labels the project records beyond the closed list (C5). */
57
+ labels?: readonly string[];
58
+ /** Test override of `siteSystem.freeQuotes`: "Get a free quote" when true, "Get a quote" when false (C5). */
59
+ freeQuotes?: boolean;
60
+ /** Test override of `siteSystem.thirdParties`: hosts (or kinds) the project records a decision for (B1). */
61
+ recordedThirdParties?: readonly string[];
62
+ /** Claims the claims register holds evidence for: a pattern id such as "since-year" or the phrase itself (E8). No config home yet. */
63
+ recordedClaims?: readonly string[];
64
+ /** Test override of `siteSystem.phones`: further numbers the business records, such as a second line (C10). */
65
+ recordedPhones?: readonly string[];
66
+ /** The date licences are judged on (T3), default now. */
67
+ asOf?: Date;
68
+ /** Page paths that advertise no licensed work, beyond the legal and utility pages (T3). */
69
+ licenceExemptPaths?: readonly string[];
70
+ /** Replaces the default asset reader (project build output, then the base URL) for B1. */
71
+ readAsset?: ShellAssetReader;
72
+ }
73
+ export type ShellCheckId = Extract<SiteSystemCheckId, "N2" | "K7" | "C5" | "C9" | "C10" | "E2" | "E8" | "T3" | "T4" | "B1" | "L10-hero">;
74
+ export declare function passFinding(checkId: ShellCheckId, message: string, target?: string): ShellFinding;
75
+ export declare function warningFinding(checkId: ShellCheckId, message: string, target?: string): ShellFinding;
76
+ export declare function failFinding(checkId: ShellCheckId, target: string, message: string): ShellFinding;
77
+ export declare function notApplicableFinding(checkId: ShellCheckId, message: string): ShellFinding;
78
+ /** "1 sampled page" or "3 sampled pages". */
79
+ export declare function sampledPages(count: number): string;
80
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export function passFinding(checkId, message, target) {
2
+ return target === undefined ? { checkId, status: "pass", message } : { checkId, status: "pass", target, message };
3
+ }
4
+ export function warningFinding(checkId, message, target) {
5
+ return { ...passFinding(checkId, message, target), severity: "warning" };
6
+ }
7
+ export function failFinding(checkId, target, message) {
8
+ return { checkId, status: "fail", target, message };
9
+ }
10
+ export function notApplicableFinding(checkId, message) {
11
+ return { checkId, status: "not-applicable", message };
12
+ }
13
+ /** "1 sampled page" or "3 sampled pages". */
14
+ export function sampledPages(count) {
15
+ return `${count} sampled page${count === 1 ? "" : "s"}`;
16
+ }
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/site-system/rules/shell/types.ts"],"names":[],"mappings":"AAwEA,MAAM,UAAU,WAAW,CAAC,OAAqB,EAAE,OAAe,EAAE,MAAe;IACjF,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACpH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAqB,EAAE,OAAe,EAAE,MAAe;IACpF,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAqB,EAAE,MAAc,EAAE,OAAe;IAChF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAqB,EAAE,OAAe;IACzE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;AACxD,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,KAAK,gBAAgB,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC1D,CAAC","sourcesContent":["import type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport type { SiteSystemSettings } from \"../../config.js\";\n\n/** What a rendered runner receives: the sampled pages' server HTML and where the project lives. */\nexport interface ShellRenderedInput {\n /** The origin the pages were fetched from; first-party means this host. */\n baseUrl: string;\n pages: readonly { path: string; html: string }[];\n projectRoot: string;\n /** The gate's parsed `siteSystem` settings (null when not declared); read from brandlab.config.ts when absent. */\n siteSystem?: SiteSystemSettings | null;\n}\n\n/** The runner shape `brandlab-ready` calls (the same as the gate's `SiteSystemRenderedRunner`). */\nexport type ShellRenderedRunner = (input: ShellRenderedInput) => Promise<readonly SiteSystemFinding[]>;\n\n/**\n * A finding, optionally marked as a warning. A warning is reported with status \"pass\" because the finding type has\n * no warning status: it advises and never blocks.\n */\nexport interface ShellFinding extends SiteSystemFinding {\n readonly severity?: \"warning\";\n}\n\nexport interface ShellLicenceFact {\n name: string;\n identifier: string;\n jurisdiction?: string;\n /** The licensee, when the licence record names one that differs from the trading name. */\n holder?: string;\n /** ISO dates, judged by site-core's licenceRenderState: an expired licence never renders. */\n expiresOn?: string;\n checkedOn?: string;\n}\n\n/** The parts of the project's business facts the shell rules compare the pages against. */\nexport interface ShellProjectFacts {\n tradingName?: string;\n phone?: { display: string; e164: string; telHref: string };\n licences: readonly ShellLicenceFact[];\n /** Why the licence data could not be read, when it exists but is not plain data: T3 then does not run. */\n licencesUnreadable?: string;\n /** Project-relative files the facts were read from. */\n sources: readonly string[];\n}\n\n/** Reads the bytes behind a URL the page references: a script, a stylesheet or the hero image. */\nexport type ShellAssetReader = (url: URL, context: { projectRoot: string; baseUrl: string }) => Promise<Uint8Array | undefined>;\n\nexport interface ShellRuleOptions {\n /** The business facts, when the caller has them; otherwise they are read from `src/data` in the project. */\n facts?: ShellProjectFacts | null;\n /** Test override of `siteSystem.labels`: quote labels the project records beyond the closed list (C5). */\n labels?: readonly string[];\n /** Test override of `siteSystem.freeQuotes`: \"Get a free quote\" when true, \"Get a quote\" when false (C5). */\n freeQuotes?: boolean;\n /** Test override of `siteSystem.thirdParties`: hosts (or kinds) the project records a decision for (B1). */\n recordedThirdParties?: readonly string[];\n /** Claims the claims register holds evidence for: a pattern id such as \"since-year\" or the phrase itself (E8). No config home yet. */\n recordedClaims?: readonly string[];\n /** Test override of `siteSystem.phones`: further numbers the business records, such as a second line (C10). */\n recordedPhones?: readonly string[];\n /** The date licences are judged on (T3), default now. */\n asOf?: Date;\n /** Page paths that advertise no licensed work, beyond the legal and utility pages (T3). */\n licenceExemptPaths?: readonly string[];\n /** Replaces the default asset reader (project build output, then the base URL) for B1. */\n readAsset?: ShellAssetReader;\n}\n\nexport type ShellCheckId = Extract<SiteSystemCheckId, \"N2\" | \"K7\" | \"C5\" | \"C9\" | \"C10\" | \"E2\" | \"E8\" | \"T3\" | \"T4\" | \"B1\" | \"L10-hero\">;\n\nexport function passFinding(checkId: ShellCheckId, message: string, target?: string): ShellFinding {\n return target === undefined ? { checkId, status: \"pass\", message } : { checkId, status: \"pass\", target, message };\n}\n\nexport function warningFinding(checkId: ShellCheckId, message: string, target?: string): ShellFinding {\n return { ...passFinding(checkId, message, target), severity: \"warning\" };\n}\n\nexport function failFinding(checkId: ShellCheckId, target: string, message: string): ShellFinding {\n return { checkId, status: \"fail\", target, message };\n}\n\nexport function notApplicableFinding(checkId: ShellCheckId, message: string): ShellFinding {\n return { checkId, status: \"not-applicable\", message };\n}\n\n/** \"1 sampled page\" or \"3 sampled pages\". */\nexport function sampledPages(count: number): string {\n return `${count} sampled page${count === 1 ? \"\" : \"s\"}`;\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The rule-module interface of the ready gate. Each module under ./rules/ exports a runner, listed in
3
+ * ./rule-modules.ts; `brandlab-ready` calls every runner with the sampled pages and merges what they find. Static
4
+ * rules use the same shape and read the project through `projectRoot`. Every runner is called even when no page
5
+ * could be read: static rules still run, and rendered rules return not-applicable for an empty page list (the gate
6
+ * fails the run itself when home or the contact page is missing).
7
+ */
8
+ import { type SiteSystemFinding } from "./checks.js";
9
+ import type { SiteSystemSettings } from "./config.js";
10
+ import type { SiteSystemPage } from "./samples.js";
11
+ export interface SiteSystemRenderedInput {
12
+ /** The running site's base URL with --base-url; empty when the pages were read from the build output. */
13
+ readonly baseUrl: string;
14
+ readonly pages: readonly SiteSystemPage[];
15
+ /** Planned sample paths with no HTML, so rules can say what they could not check. */
16
+ readonly missing: readonly string[];
17
+ /** The project directory, for rules that read its data files. */
18
+ readonly projectRoot: string;
19
+ /**
20
+ * The gate's parsed `siteSystem` settings from brandlab.config.ts (null when the site declares none). Rules use these
21
+ * rather than reading the config themselves; a caller that leaves it out lets each rule read the config.
22
+ */
23
+ readonly siteSystem?: SiteSystemSettings | null;
24
+ }
25
+ /** A rule module: one finding per check result (pass, fail or not-applicable), never a throw for a failing site. */
26
+ export type SiteSystemRenderedRunner = (input: SiteSystemRenderedInput) => Promise<readonly SiteSystemFinding[]>;
27
+ /**
28
+ * Runs each runner in turn. A runner that throws, or returns a finding for an unknown check, is reported as a
29
+ * problem; the checks it would have covered then show as not run, which fails them where they block.
30
+ */
31
+ export declare function runSiteSystemRenderedRunners(runners: readonly SiteSystemRenderedRunner[], input: SiteSystemRenderedInput): Promise<{
32
+ findings: SiteSystemFinding[];
33
+ problems: string[];
34
+ }>;
35
+ /** A finding with a known check id, a known status and a message; anything else is ignored. */
36
+ export declare function isSiteSystemFinding(value: unknown): value is SiteSystemFinding;
37
+ //# sourceMappingURL=runners.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runners.d.ts","sourceRoot":"","sources":["../../src/site-system/runners.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,WAAW,uBAAuB;IACtC,yGAAyG;IACzG,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACjD;AAED,oHAAoH;AACpH,MAAM,MAAM,wBAAwB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;AAIjH;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,SAAS,wBAAwB,EAAE,EAC5C,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC;IAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAkBhE;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAW9E"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The rule-module interface of the ready gate. Each module under ./rules/ exports a runner, listed in
3
+ * ./rule-modules.ts; `brandlab-ready` calls every runner with the sampled pages and merges what they find. Static
4
+ * rules use the same shape and read the project through `projectRoot`. Every runner is called even when no page
5
+ * could be read: static rules still run, and rendered rules return not-applicable for an empty page list (the gate
6
+ * fails the run itself when home or the contact page is missing).
7
+ */
8
+ import { SITE_SYSTEM_CHECKS } from "./checks.js";
9
+ const KNOWN_IDS = new Set(SITE_SYSTEM_CHECKS.map((check) => check.id));
10
+ /**
11
+ * Runs each runner in turn. A runner that throws, or returns a finding for an unknown check, is reported as a
12
+ * problem; the checks it would have covered then show as not run, which fails them where they block.
13
+ */
14
+ export async function runSiteSystemRenderedRunners(runners, input) {
15
+ const findings = [];
16
+ const problems = [];
17
+ for (const runner of runners) {
18
+ const name = runner.name || "A rule module";
19
+ let results;
20
+ try {
21
+ results = await runner(input);
22
+ }
23
+ catch (error) {
24
+ problems.push(`${name} stopped with an error (${error instanceof Error ? error.message : String(error)}), so its checks did not run.`);
25
+ continue;
26
+ }
27
+ for (const finding of results) {
28
+ if (isSiteSystemFinding(finding))
29
+ findings.push(finding);
30
+ else
31
+ problems.push(`${name} returned a result for an unknown check or with an unknown status, so it was ignored.`);
32
+ }
33
+ }
34
+ return { findings, problems };
35
+ }
36
+ /** A finding with a known check id, a known status and a message; anything else is ignored. */
37
+ export function isSiteSystemFinding(value) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value))
39
+ return false;
40
+ const finding = value;
41
+ return (typeof finding.checkId === "string" &&
42
+ KNOWN_IDS.has(finding.checkId) &&
43
+ (finding.status === "pass" || finding.status === "fail" || finding.status === "not-applicable") &&
44
+ typeof finding.message === "string" &&
45
+ (finding.target === undefined || typeof finding.target === "string") &&
46
+ (finding.severity === undefined || finding.severity === "warning"));
47
+ }
48
+ //# sourceMappingURL=runners.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runners.js","sourceRoot":"","sources":["../../src/site-system/runners.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAA0B,MAAM,aAAa,CAAC;AAsBzE,MAAM,SAAS,GAAG,IAAI,GAAG,CAAS,kBAAkB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;AAE/E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAA4C,EAC5C,KAA8B;IAE9B,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,eAAe,CAAC;QAC5C,IAAI,OAAqC,CAAC;QAC1C,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,2BAA2B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACvI,SAAS;QACX,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,mBAAmB,CAAC,OAAO,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;;gBACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,uFAAuF,CAAC,CAAC;QACrH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,MAAM,OAAO,GAAG,KAAgC,CAAC;IACjD,OAAO,CACL,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QACnC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;QAC9B,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,OAAO,CAAC,MAAM,KAAK,gBAAgB,CAAC;QAC/F,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QACnC,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC;QACpE,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CACnE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The rule-module interface of the ready gate. Each module under ./rules/ exports a runner, listed in\n * ./rule-modules.ts; `brandlab-ready` calls every runner with the sampled pages and merges what they find. Static\n * rules use the same shape and read the project through `projectRoot`. Every runner is called even when no page\n * could be read: static rules still run, and rendered rules return not-applicable for an empty page list (the gate\n * fails the run itself when home or the contact page is missing).\n */\nimport { SITE_SYSTEM_CHECKS, type SiteSystemFinding } from \"./checks.js\";\nimport type { SiteSystemSettings } from \"./config.js\";\nimport type { SiteSystemPage } from \"./samples.js\";\n\nexport interface SiteSystemRenderedInput {\n /** The running site's base URL with --base-url; empty when the pages were read from the build output. */\n readonly baseUrl: string;\n readonly pages: readonly SiteSystemPage[];\n /** Planned sample paths with no HTML, so rules can say what they could not check. */\n readonly missing: readonly string[];\n /** The project directory, for rules that read its data files. */\n readonly projectRoot: string;\n /**\n * The gate's parsed `siteSystem` settings from brandlab.config.ts (null when the site declares none). Rules use these\n * rather than reading the config themselves; a caller that leaves it out lets each rule read the config.\n */\n readonly siteSystem?: SiteSystemSettings | null;\n}\n\n/** A rule module: one finding per check result (pass, fail or not-applicable), never a throw for a failing site. */\nexport type SiteSystemRenderedRunner = (input: SiteSystemRenderedInput) => Promise<readonly SiteSystemFinding[]>;\n\nconst KNOWN_IDS = new Set<string>(SITE_SYSTEM_CHECKS.map((check) => check.id));\n\n/**\n * Runs each runner in turn. A runner that throws, or returns a finding for an unknown check, is reported as a\n * problem; the checks it would have covered then show as not run, which fails them where they block.\n */\nexport async function runSiteSystemRenderedRunners(\n runners: readonly SiteSystemRenderedRunner[],\n input: SiteSystemRenderedInput,\n): Promise<{ findings: SiteSystemFinding[]; problems: string[] }> {\n const findings: SiteSystemFinding[] = [];\n const problems: string[] = [];\n for (const runner of runners) {\n const name = runner.name || \"A rule module\";\n let results: readonly SiteSystemFinding[];\n try {\n results = await runner(input);\n } catch (error) {\n problems.push(`${name} stopped with an error (${error instanceof Error ? error.message : String(error)}), so its checks did not run.`);\n continue;\n }\n for (const finding of results) {\n if (isSiteSystemFinding(finding)) findings.push(finding);\n else problems.push(`${name} returned a result for an unknown check or with an unknown status, so it was ignored.`);\n }\n }\n return { findings, problems };\n}\n\n/** A finding with a known check id, a known status and a message; anything else is ignored. */\nexport function isSiteSystemFinding(value: unknown): value is SiteSystemFinding {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const finding = value as Record<string, unknown>;\n return (\n typeof finding.checkId === \"string\" &&\n KNOWN_IDS.has(finding.checkId) &&\n (finding.status === \"pass\" || finding.status === \"fail\" || finding.status === \"not-applicable\") &&\n typeof finding.message === \"string\" &&\n (finding.target === undefined || typeof finding.target === \"string\") &&\n (finding.severity === undefined || finding.severity === \"warning\")\n );\n}\n"]}
@@ -0,0 +1,43 @@
1
+ export { resolveStreamedHtml } from "./streamed-html.js";
2
+ export interface SiteSystemPage {
3
+ /** Root-relative path, for example `/services/roofing`. */
4
+ readonly path: string;
5
+ readonly html: string;
6
+ }
7
+ /** The fetch subset the loader uses, so tests can stand in for a site. */
8
+ export type SiteSystemFetch = (url: string, init: {
9
+ headers: Record<string, string>;
10
+ redirect: "follow";
11
+ signal?: AbortSignal;
12
+ }) => Promise<{
13
+ ok: boolean;
14
+ status: number;
15
+ headers: {
16
+ get(name: string): string | null;
17
+ };
18
+ text(): Promise<string>;
19
+ }>;
20
+ export interface SiteSystemPageLoad {
21
+ readonly pages: readonly SiteSystemPage[];
22
+ /** Planned paths with no HTML: no prerendered file, or no 200 HTML answer from the base URL. */
23
+ readonly missing: readonly string[];
24
+ /** Where the HTML came from. */
25
+ readonly source: "base-url" | "build";
26
+ readonly problems: readonly string[];
27
+ }
28
+ /** Sample paths in a stable order, without duplicates. `prerenderManifest` is `.next/prerender-manifest.json`. */
29
+ export declare function planSiteSystemSamplePaths(input: {
30
+ representativeRoutes: Readonly<Record<string, string>>;
31
+ samples?: readonly string[];
32
+ prerenderManifest?: unknown;
33
+ }): string[];
34
+ /** The prerendered HTML file Next writes for a path. */
35
+ export declare function builtPageFile(buildDir: string, path: string): string;
36
+ export declare function loadSiteSystemPages(input: {
37
+ paths: readonly string[];
38
+ /** Absolute build directory (`.next`). */
39
+ buildDir: string;
40
+ baseUrl?: string;
41
+ fetch?: SiteSystemFetch;
42
+ }): Promise<SiteSystemPageLoad>;
43
+ //# sourceMappingURL=samples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../../src/site-system/samples.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAChF,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAEtH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAMD,kHAAkH;AAClH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAC/C,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,MAAM,EAAE,CA4BX;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE;IAC/C,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC9B"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The pages the rendered checks read: home, the contact page, the other representative routes, one page for each
3
+ * other route of the production build (the first path Next prerendered for it, which comes from the route
4
+ * catalogue) and the site's `siteSystem.samples`. With a base URL they are fetched from the running site;
5
+ * otherwise their prerendered HTML is read from the build output (`.next/server/app/<path>.html`). Either way the
6
+ * streamed Suspense segments are put in place first (`resolveStreamedHtml`), so every runner reads the content a
7
+ * browser shows, not the fallback.
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { resolveStreamedHtml } from "./streamed-html.js";
12
+ export { resolveStreamedHtml } from "./streamed-html.js";
13
+ const REPRESENTATIVE_ORDER = ["home", "contact", "service", "location", "blog"];
14
+ const INTERNAL_ROUTES = new Set(["/_not-found", "/_global-error", "/404", "/500"]);
15
+ const FETCH_TIMEOUT_MS = 30_000;
16
+ /** Sample paths in a stable order, without duplicates. `prerenderManifest` is `.next/prerender-manifest.json`. */
17
+ export function planSiteSystemSamplePaths(input) {
18
+ const paths = ["/"];
19
+ const representative = Object.entries(input.representativeRoutes).sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right));
20
+ for (const [, path] of representative)
21
+ paths.push(path);
22
+ const routes = isRecord(input.prerenderManifest) && isRecord(input.prerenderManifest.routes)
23
+ ? input.prerenderManifest.routes
24
+ : {};
25
+ const appRouteOf = (path) => {
26
+ const entry = routes[path];
27
+ return isRecord(entry) && typeof entry.srcRoute === "string" ? entry.srcRoute : undefined;
28
+ };
29
+ // A route a representative page or a sample already covers needs no second page.
30
+ const seenRoutes = new Set([...paths, ...(input.samples ?? [])].flatMap((path) => appRouteOf(path) ?? []));
31
+ for (const [path, entry] of Object.entries(routes)) {
32
+ if (!isRecord(entry) || !path.startsWith("/"))
33
+ continue;
34
+ const appRoute = typeof entry.srcRoute === "string" ? entry.srcRoute : path;
35
+ if (INTERNAL_ROUTES.has(appRoute) || seenRoutes.has(appRoute))
36
+ continue;
37
+ // Next 15+ marks route handlers (robots, sitemaps, icons); older manifests only show them by their extension.
38
+ if (entry.routeType !== undefined ? entry.routeType !== "page" : /\.[a-z0-9]+$/i.test(path))
39
+ continue;
40
+ seenRoutes.add(appRoute);
41
+ paths.push(path);
42
+ }
43
+ paths.push(...(input.samples ?? []));
44
+ return [...new Set(paths)];
45
+ }
46
+ /** The prerendered HTML file Next writes for a path. */
47
+ export function builtPageFile(buildDir, path) {
48
+ return join(buildDir, "server", "app", `${path === "/" ? "/index" : path.replace(/\/+$/, "")}.html`);
49
+ }
50
+ export async function loadSiteSystemPages(input) {
51
+ const pages = [];
52
+ const missing = [];
53
+ const problems = [];
54
+ if (input.baseUrl) {
55
+ const fetcher = input.fetch ?? globalThis.fetch;
56
+ const base = input.baseUrl.replace(/\/+$/, "");
57
+ for (const path of input.paths) {
58
+ try {
59
+ const response = await fetcher(`${base}${path}`, {
60
+ headers: { accept: "text/html" },
61
+ redirect: "follow",
62
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
63
+ });
64
+ const type = response.headers.get("content-type") ?? "";
65
+ if (response.ok && (!type || type.includes("text/html"))) {
66
+ pages.push({ path, html: resolveStreamedHtml(await response.text()) });
67
+ continue;
68
+ }
69
+ problems.push(response.ok
70
+ ? `GET ${path} answered ${type}, not HTML.`
71
+ : `GET ${path} answered ${response.status}.`);
72
+ }
73
+ catch (error) {
74
+ problems.push(`GET ${path} failed (${error instanceof Error ? error.message : String(error)}).`);
75
+ }
76
+ missing.push(path);
77
+ }
78
+ return { pages, missing, source: "base-url", problems };
79
+ }
80
+ for (const path of input.paths) {
81
+ const file = builtPageFile(input.buildDir, path);
82
+ if (existsSync(file))
83
+ pages.push({ path, html: resolveStreamedHtml(readFileSync(file, "utf8")) });
84
+ else
85
+ missing.push(path);
86
+ }
87
+ if (missing.length > 0) {
88
+ problems.push(`No prerendered HTML in the build for ${missing.join(", ")}; pass --base-url to fetch them from the running site.`);
89
+ }
90
+ return { pages, missing, source: "build", problems };
91
+ }
92
+ function rank(name) {
93
+ const index = REPRESENTATIVE_ORDER.indexOf(name);
94
+ return index === -1 ? REPRESENTATIVE_ORDER.length : index;
95
+ }
96
+ function isRecord(value) {
97
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
98
+ }
99
+ //# sourceMappingURL=samples.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"samples.js","sourceRoot":"","sources":["../../src/site-system/samples.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pages the rendered checks read: home, the contact page, the other representative routes, one page for each\n * other route of the production build (the first path Next prerendered for it, which comes from the route\n * catalogue) and the site's `siteSystem.samples`. With a base URL they are fetched from the running site;\n * otherwise their prerendered HTML is read from the build output (`.next/server/app/<path>.html`). Either way the\n * streamed Suspense segments are put in place first (`resolveStreamedHtml`), so every runner reads the content a\n * browser shows, not the fallback.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolveStreamedHtml } from \"./streamed-html.js\";\n\nexport { resolveStreamedHtml } from \"./streamed-html.js\";\n\nexport interface SiteSystemPage {\n /** Root-relative path, for example `/services/roofing`. */\n readonly path: string;\n readonly html: string;\n}\n\n/** The fetch subset the loader uses, so tests can stand in for a site. */\nexport type SiteSystemFetch = (\n url: string,\n init: { headers: Record<string, string>; redirect: \"follow\"; signal?: AbortSignal },\n) => Promise<{ ok: boolean; status: number; headers: { get(name: string): string | null }; text(): Promise<string> }>;\n\nexport interface SiteSystemPageLoad {\n readonly pages: readonly SiteSystemPage[];\n /** Planned paths with no HTML: no prerendered file, or no 200 HTML answer from the base URL. */\n readonly missing: readonly string[];\n /** Where the HTML came from. */\n readonly source: \"base-url\" | \"build\";\n readonly problems: readonly string[];\n}\n\nconst REPRESENTATIVE_ORDER = [\"home\", \"contact\", \"service\", \"location\", \"blog\"];\nconst INTERNAL_ROUTES = new Set([\"/_not-found\", \"/_global-error\", \"/404\", \"/500\"]);\nconst FETCH_TIMEOUT_MS = 30_000;\n\n/** Sample paths in a stable order, without duplicates. `prerenderManifest` is `.next/prerender-manifest.json`. */\nexport function planSiteSystemSamplePaths(input: {\n representativeRoutes: Readonly<Record<string, string>>;\n samples?: readonly string[];\n prerenderManifest?: unknown;\n}): string[] {\n const paths = [\"/\"];\n const representative = Object.entries(input.representativeRoutes).sort(\n ([left], [right]) => rank(left) - rank(right) || left.localeCompare(right),\n );\n for (const [, path] of representative) paths.push(path);\n const routes = isRecord(input.prerenderManifest) && isRecord(input.prerenderManifest.routes)\n ? input.prerenderManifest.routes\n : {};\n const appRouteOf = (path: string) => {\n const entry = routes[path];\n return isRecord(entry) && typeof entry.srcRoute === \"string\" ? entry.srcRoute : undefined;\n };\n // A route a representative page or a sample already covers needs no second page.\n const seenRoutes = new Set<string>(\n [...paths, ...(input.samples ?? [])].flatMap((path) => appRouteOf(path) ?? []),\n );\n for (const [path, entry] of Object.entries(routes)) {\n if (!isRecord(entry) || !path.startsWith(\"/\")) continue;\n const appRoute = typeof entry.srcRoute === \"string\" ? entry.srcRoute : path;\n if (INTERNAL_ROUTES.has(appRoute) || seenRoutes.has(appRoute)) continue;\n // Next 15+ marks route handlers (robots, sitemaps, icons); older manifests only show them by their extension.\n if (entry.routeType !== undefined ? entry.routeType !== \"page\" : /\\.[a-z0-9]+$/i.test(path)) continue;\n seenRoutes.add(appRoute);\n paths.push(path);\n }\n paths.push(...(input.samples ?? []));\n return [...new Set(paths)];\n}\n\n/** The prerendered HTML file Next writes for a path. */\nexport function builtPageFile(buildDir: string, path: string): string {\n return join(buildDir, \"server\", \"app\", `${path === \"/\" ? \"/index\" : path.replace(/\\/+$/, \"\")}.html`);\n}\n\nexport async function loadSiteSystemPages(input: {\n paths: readonly string[];\n /** Absolute build directory (`.next`). */\n buildDir: string;\n baseUrl?: string;\n fetch?: SiteSystemFetch;\n}): Promise<SiteSystemPageLoad> {\n const pages: SiteSystemPage[] = [];\n const missing: string[] = [];\n const problems: string[] = [];\n if (input.baseUrl) {\n const fetcher = input.fetch ?? (globalThis.fetch as unknown as SiteSystemFetch);\n const base = input.baseUrl.replace(/\\/+$/, \"\");\n for (const path of input.paths) {\n try {\n const response = await fetcher(`${base}${path}`, {\n headers: { accept: \"text/html\" },\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n const type = response.headers.get(\"content-type\") ?? \"\";\n if (response.ok && (!type || type.includes(\"text/html\"))) {\n pages.push({ path, html: resolveStreamedHtml(await response.text()) });\n continue;\n }\n problems.push(response.ok\n ? `GET ${path} answered ${type}, not HTML.`\n : `GET ${path} answered ${response.status}.`);\n } catch (error) {\n problems.push(`GET ${path} failed (${error instanceof Error ? error.message : String(error)}).`);\n }\n missing.push(path);\n }\n return { pages, missing, source: \"base-url\", problems };\n }\n for (const path of input.paths) {\n const file = builtPageFile(input.buildDir, path);\n if (existsSync(file)) pages.push({ path, html: resolveStreamedHtml(readFileSync(file, \"utf8\")) });\n else missing.push(path);\n }\n if (missing.length > 0) {\n problems.push(`No prerendered HTML in the build for ${missing.join(\", \")}; pass --base-url to fetch them from the running site.`);\n }\n return { pages, missing, source: \"build\", problems };\n}\n\nfunction rank(name: string): number {\n const index = REPRESENTATIVE_ORDER.indexOf(name);\n return index === -1 ? REPRESENTATIVE_ORDER.length : index;\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,66 @@
1
+ export interface SiteSystemStep {
2
+ readonly id: string;
3
+ readonly summary: string;
4
+ }
5
+ export declare const SITE_SYSTEM_LAUNCH_STEPS: readonly [{
6
+ readonly id: "T2";
7
+ readonly summary: "The Business Profile matches the site: name, phone, primary category, services, service area, hours and description.";
8
+ }, {
9
+ readonly id: "T3-R";
10
+ readonly summary: "Each licence checked on the NSW register within 30 days and not expired (warning inside 60 days of expiry).";
11
+ }, {
12
+ readonly id: "C6";
13
+ readonly summary: "Launch proof: a delivered test enquiry with a photo, the contact_form hit, a test phone tap and the next-day GA4 read; the owner saw it; the test marker kept every hit out of reporting.";
14
+ }, {
15
+ readonly id: "X3";
16
+ readonly summary: "The Codex second opinion (codex review --base main); real findings fixed or answered.";
17
+ }, {
18
+ readonly id: "X4";
19
+ readonly summary: "Production environment variable names present (values never read) and the domain resolves.";
20
+ }, {
21
+ readonly id: "X5";
22
+ readonly summary: "Rollback recorded and the cutover inside the window.";
23
+ }, {
24
+ readonly id: "X6";
25
+ readonly summary: "The legacy security sweep is done.";
26
+ }, {
27
+ readonly id: "X7";
28
+ readonly summary: "Ads final URLs and the Business Profile's tagged website URL mapped and handed to Codex.";
29
+ }, {
30
+ readonly id: "X8";
31
+ readonly summary: "No sentence shared with a related or same-trade client's site, library strings included.";
32
+ }, {
33
+ readonly id: "X9";
34
+ readonly summary: "DNS diff before and after cutover (MX, SPF, DKIM, DMARC, verification TXT) and a test email into and out of the client's mailbox.";
35
+ }, {
36
+ readonly id: "X10";
37
+ readonly summary: "The sending domain is verified in Resend with SPF and DKIM, and DMARC is present.";
38
+ }];
39
+ export declare const SITE_SYSTEM_HUMAN_STEPS: readonly [{
40
+ readonly id: "truth-pass";
41
+ readonly summary: "Truth pass (the builder, 30 to 60 min): the claims and evidence table.";
42
+ }, {
43
+ readonly id: "trust-carry-over";
44
+ readonly summary: "Trust carry-over (the builder, 20 min): every old badge, claim, price and review block kept, or held with a reason.";
45
+ }, {
46
+ readonly id: "usefulness-read";
47
+ readonly summary: "Head-page usefulness test and 5-page side-by-side read per family (the builder, 20 min per family): pass, or the pages fixed.";
48
+ }, {
49
+ readonly id: "codex-second-opinion";
50
+ readonly summary: "Codex second opinion (one run, one fix round): findings, then fixed or answered.";
51
+ }, {
52
+ readonly id: "launch-proof";
53
+ readonly summary: "Launch proof (the builder, 10 min): the delivered email timestamp and the captured hits.";
54
+ }, {
55
+ readonly id: "look-pick";
56
+ readonly summary: "Look pick and side-by-side packs (Romie, one reply each): A, B, yes or a note.";
57
+ }];
58
+ export type SiteSystemLaunchStepId = (typeof SITE_SYSTEM_LAUNCH_STEPS)[number]["id"];
59
+ export type SiteSystemHumanStepId = (typeof SITE_SYSTEM_HUMAN_STEPS)[number]["id"];
60
+ /** `launch` is docs/ready/launch.json, `human` is docs/ready/human.json. */
61
+ export type SiteSystemRecordFile = "launch" | "human";
62
+ /** Registry checks that are recorded rather than run (K10 harness parity, X1 legacy URLs). */
63
+ export declare const SITE_SYSTEM_RECORDED_CHECK_IDS: readonly string[];
64
+ /** The records file an id is recorded in, or null when the id is not a step or a recorded check. */
65
+ export declare function siteSystemRecordFile(id: string): SiteSystemRecordFile | null;
66
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/site-system/steps.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYS,CAAC;AAE/C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;EAOU,CAAC;AAE/C,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AACrF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAEnF,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtD,8FAA8F;AAC9F,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAEjC,CAAC;AAK5B,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAI5E"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The launch-day agent steps (§13.3) and the time-boxed human steps (§13.6) the ready report lists.
3
+ *
4
+ * Each is recorded with `brandlab-ready record <id> --evidence <ref>`: launch-day steps, and the registry's
5
+ * `record` checks (K10, X1), go to docs/ready/launch.json; human steps go to docs/ready/human.json. A site is
6
+ * launch-ready only when every step is recorded; a default run reports missing steps without failing, and
7
+ * `brandlab-ready --launch` fails until they are all recorded.
8
+ */
9
+ import { SITE_SYSTEM_CHECKS } from "./checks.js";
10
+ export const SITE_SYSTEM_LAUNCH_STEPS = [
11
+ { id: "T2", summary: "The Business Profile matches the site: name, phone, primary category, services, service area, hours and description." },
12
+ { id: "T3-R", summary: "Each licence checked on the NSW register within 30 days and not expired (warning inside 60 days of expiry)." },
13
+ { id: "C6", summary: "Launch proof: a delivered test enquiry with a photo, the contact_form hit, a test phone tap and the next-day GA4 read; the owner saw it; the test marker kept every hit out of reporting." },
14
+ { id: "X3", summary: "The Codex second opinion (codex review --base main); real findings fixed or answered." },
15
+ { id: "X4", summary: "Production environment variable names present (values never read) and the domain resolves." },
16
+ { id: "X5", summary: "Rollback recorded and the cutover inside the window." },
17
+ { id: "X6", summary: "The legacy security sweep is done." },
18
+ { id: "X7", summary: "Ads final URLs and the Business Profile's tagged website URL mapped and handed to Codex." },
19
+ { id: "X8", summary: "No sentence shared with a related or same-trade client's site, library strings included." },
20
+ { id: "X9", summary: "DNS diff before and after cutover (MX, SPF, DKIM, DMARC, verification TXT) and a test email into and out of the client's mailbox." },
21
+ { id: "X10", summary: "The sending domain is verified in Resend with SPF and DKIM, and DMARC is present." },
22
+ ];
23
+ export const SITE_SYSTEM_HUMAN_STEPS = [
24
+ { id: "truth-pass", summary: "Truth pass (the builder, 30 to 60 min): the claims and evidence table." },
25
+ { id: "trust-carry-over", summary: "Trust carry-over (the builder, 20 min): every old badge, claim, price and review block kept, or held with a reason." },
26
+ { id: "usefulness-read", summary: "Head-page usefulness test and 5-page side-by-side read per family (the builder, 20 min per family): pass, or the pages fixed." },
27
+ { id: "codex-second-opinion", summary: "Codex second opinion (one run, one fix round): findings, then fixed or answered." },
28
+ { id: "launch-proof", summary: "Launch proof (the builder, 10 min): the delivered email timestamp and the captured hits." },
29
+ { id: "look-pick", summary: "Look pick and side-by-side packs (Romie, one reply each): A, B, yes or a note." },
30
+ ];
31
+ /** Registry checks that are recorded rather than run (K10 harness parity, X1 legacy URLs). */
32
+ export const SITE_SYSTEM_RECORDED_CHECK_IDS = SITE_SYSTEM_CHECKS
33
+ .filter((check) => check.kind === "record")
34
+ .map((check) => check.id);
35
+ const LAUNCH_IDS = new Set([...SITE_SYSTEM_LAUNCH_STEPS.map((step) => step.id), ...SITE_SYSTEM_RECORDED_CHECK_IDS]);
36
+ const HUMAN_IDS = new Set(SITE_SYSTEM_HUMAN_STEPS.map((step) => step.id));
37
+ /** The records file an id is recorded in, or null when the id is not a step or a recorded check. */
38
+ export function siteSystemRecordFile(id) {
39
+ if (LAUNCH_IDS.has(id))
40
+ return "launch";
41
+ if (HUMAN_IDS.has(id))
42
+ return "human";
43
+ return null;
44
+ }
45
+ //# sourceMappingURL=steps.js.map