@brandlab-digital/conformance 3.13.0 → 3.15.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 (152) hide show
  1. package/README.md +68 -2
  2. package/dist/analytics-e2e.d.ts +10 -16
  3. package/dist/analytics-e2e.d.ts.map +1 -1
  4. package/dist/analytics-e2e.js +61 -3
  5. package/dist/analytics-e2e.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/current-release.js +5 -5
  10. package/dist/current-release.js.map +1 -1
  11. package/dist/engineering.d.ts.map +1 -1
  12. package/dist/engineering.js +18 -2
  13. package/dist/engineering.js.map +1 -1
  14. package/dist/estate-snapshot.d.ts +54 -0
  15. package/dist/estate-snapshot.d.ts.map +1 -0
  16. package/dist/estate-snapshot.js +164 -0
  17. package/dist/estate-snapshot.js.map +1 -0
  18. package/dist/estate.d.ts +38 -0
  19. package/dist/estate.d.ts.map +1 -1
  20. package/dist/estate.js +71 -28
  21. package/dist/estate.js.map +1 -1
  22. package/dist/media-snapshot.d.ts +53 -0
  23. package/dist/media-snapshot.d.ts.map +1 -0
  24. package/dist/media-snapshot.js +66 -0
  25. package/dist/media-snapshot.js.map +1 -0
  26. package/dist/project.d.ts +20 -0
  27. package/dist/project.d.ts.map +1 -1
  28. package/dist/project.js +92 -2
  29. package/dist/project.js.map +1 -1
  30. package/dist/ready-cli.d.ts.map +1 -1
  31. package/dist/ready-cli.js +8 -3
  32. package/dist/ready-cli.js.map +1 -1
  33. package/dist/rule-registry.d.ts +5 -0
  34. package/dist/rule-registry.d.ts.map +1 -1
  35. package/dist/rule-registry.js +1 -0
  36. package/dist/rule-registry.js.map +1 -1
  37. package/dist/site-system/checks.d.ts +28 -3
  38. package/dist/site-system/checks.d.ts.map +1 -1
  39. package/dist/site-system/checks.js +5 -2
  40. package/dist/site-system/checks.js.map +1 -1
  41. package/dist/site-system/config.d.ts +13 -0
  42. package/dist/site-system/config.d.ts.map +1 -1
  43. package/dist/site-system/config.js +30 -1
  44. package/dist/site-system/config.js.map +1 -1
  45. package/dist/site-system/index.d.ts +1 -1
  46. package/dist/site-system/index.d.ts.map +1 -1
  47. package/dist/site-system/index.js +1 -1
  48. package/dist/site-system/index.js.map +1 -1
  49. package/dist/site-system/journeys/conversion.d.ts.map +1 -1
  50. package/dist/site-system/journeys/conversion.js +15 -2
  51. package/dist/site-system/journeys/conversion.js.map +1 -1
  52. package/dist/site-system/journeys/menu.d.ts.map +1 -1
  53. package/dist/site-system/journeys/menu.js +24 -1
  54. package/dist/site-system/journeys/menu.js.map +1 -1
  55. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
  56. package/dist/site-system/journeys/page-helpers.js +1 -1
  57. package/dist/site-system/journeys/page-helpers.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +13 -3
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +12 -2
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/shell.d.ts +1 -1
  63. package/dist/site-system/journeys/shell.js +2 -2
  64. package/dist/site-system/journeys/shell.js.map +1 -1
  65. package/dist/site-system/report.d.ts.map +1 -1
  66. package/dist/site-system/report.js +42 -8
  67. package/dist/site-system/report.js.map +1 -1
  68. package/dist/site-system/rule-modules.d.ts.map +1 -1
  69. package/dist/site-system/rule-modules.js +5 -0
  70. package/dist/site-system/rule-modules.js.map +1 -1
  71. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  72. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  73. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  74. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  75. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  76. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  77. package/dist/site-system/rules/delivery/index.js +7 -0
  78. package/dist/site-system/rules/delivery/index.js.map +1 -0
  79. package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
  80. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
  81. package/dist/site-system/rules/form/c7-thank-you.js +26 -3
  82. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
  83. package/dist/site-system/rules/form/index.d.ts.map +1 -1
  84. package/dist/site-system/rules/form/index.js +11 -1
  85. package/dist/site-system/rules/form/index.js.map +1 -1
  86. package/dist/site-system/rules/full/common.d.ts +2 -0
  87. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  88. package/dist/site-system/rules/full/common.js.map +1 -1
  89. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  90. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  91. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  92. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  93. package/dist/site-system/rules/full/estate.d.ts +26 -3
  94. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  95. package/dist/site-system/rules/full/estate.js +58 -9
  96. package/dist/site-system/rules/full/estate.js.map +1 -1
  97. package/dist/site-system/rules/full/geography.d.ts +15 -2
  98. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  99. package/dist/site-system/rules/full/geography.js +15 -2
  100. package/dist/site-system/rules/full/geography.js.map +1 -1
  101. package/dist/site-system/rules/full/index.d.ts +4 -3
  102. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  103. package/dist/site-system/rules/full/index.js +20 -5
  104. package/dist/site-system/rules/full/index.js.map +1 -1
  105. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  106. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  107. package/dist/site-system/rules/full/k12-slots.js +40 -0
  108. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  109. package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
  110. package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
  111. package/dist/site-system/rules/full/l10-location-copy.js +5 -2
  112. package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
  113. package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
  114. package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
  115. package/dist/site-system/rules/full/l3-neighbours.js +4 -1
  116. package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
  117. package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
  118. package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
  119. package/dist/site-system/rules/full/l4-local-copy.js +8 -3
  120. package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
  121. package/dist/site-system/rules/full/l9-data-contract.js +2 -2
  122. package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
  123. package/dist/site-system/rules/full/links.d.ts +3 -1
  124. package/dist/site-system/rules/full/links.d.ts.map +1 -1
  125. package/dist/site-system/rules/full/links.js +15 -5
  126. package/dist/site-system/rules/full/links.js.map +1 -1
  127. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  128. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  129. package/dist/site-system/rules/full/loc-index.js +43 -2
  130. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  131. package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
  132. package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
  133. package/dist/site-system/rules/full/route-kinds.js +52 -0
  134. package/dist/site-system/rules/full/route-kinds.js.map +1 -0
  135. package/dist/site-system/rules/full/strategy.d.ts +6 -0
  136. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  137. package/dist/site-system/rules/full/strategy.js +98 -10
  138. package/dist/site-system/rules/full/strategy.js.map +1 -1
  139. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  140. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  141. package/dist/site-system/rules/sections/k2-skin-coverage.js +13 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
  144. package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
  145. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
  146. package/dist/site-system/runners.js +1 -1
  147. package/dist/site-system/runners.js.map +1 -1
  148. package/dist/site-system-e2e.d.ts +1 -1
  149. package/dist/site-system-e2e.d.ts.map +1 -1
  150. package/dist/site-system-e2e.js +17 -4
  151. package/dist/site-system-e2e.js.map +1 -1
  152. package/package.json +10 -2
@@ -1,9 +1,9 @@
1
1
  import type { SiteSystemFinding } from "../../checks.js";
2
2
  import { type ParsedPage } from "../enquiry/common.js";
3
3
  /** `data-bl` names that have an entry in `BrandlabSkin` (ui-kit/brandlab-skin.ts). */
4
- export declare const K2_SKINNED_HOOKS: readonly ["header", "drawer", "utility", "callbar", "back-to-top", "quote-button", "phone-link", "text-link", "button", "container", "quote-form", "photo-upload", "dialog", "field", "thank-you", "rating-chip", "footer", "accordion", "price-table"];
4
+ export declare const K2_SKINNED_HOOKS: readonly ["header", "drawer", "utility", "callbar", "back-to-top", "quote-button", "phone-link", "text-link", "button", "container", "quote-form", "photo-upload", "dialog", "field", "thank-you", "rating-chip", "footer", "accordion", "price-table", "eyebrow", "card", "badge", "chip", "stat", "tick-list", "link-list", "media", "breadcrumbs", "quote-checklist"];
5
5
  /** `data-bl-section` ids of the sections the library renders and skins. */
6
- export declare const K2_SKINNED_SECTIONS: readonly ["reviews", "cta", "faqs", "price-guide", "proof-strip", "page-header", "contact"];
6
+ export declare const K2_SKINNED_SECTIONS: readonly ["reviews", "cta", "faqs", "price-guide", "proof-strip", "page-header", "contact", "hero", "services", "projects", "gallery", "before-after", "licences", "emergency"];
7
7
  export declare const K2_REFERENCE_CLASS: RegExp;
8
8
  /** Hook names the project's own stylesheets select: `[data-bl="x"]` and `[data-bl-section="x"]`. */
9
9
  export declare function hooksStyledBySite(projectRoot: string): Set<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"k2-skin-coverage.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/k2-skin-coverage.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAI1F,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,yPAGnB,CAAC;AAEX,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,6FAA8F,CAAC;AAE/H,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAqB9D,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA0BlE;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAqC9F"}
1
+ {"version":3,"file":"k2-skin-coverage.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/k2-skin-coverage.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAI1F,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,0WAOnB,CAAC;AAEX,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,iLAItB,CAAC;AAEX,eAAO,MAAM,kBAAkB,QAA8B,CAAC;AAqB9D,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA0BlE;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAqC9F"}
@@ -5,7 +5,10 @@
5
5
  * What the HTML can show, defined exactly:
6
6
  * - A component is a rendered element with `data-bl="<name>"` or `data-bl-section="<id>"`, for the names the library
7
7
  * skins (SKINNED_HOOKS and SKINNED_SECTIONS below). Hooks without a skin entry (the skip link, the honeypot, a star
8
- * row) and the site's own compositions (the hero) are not components in this sense.
8
+ * row) and the site's own compositions are not components in this sense. From 3.14.0 the library's hero and the
9
+ * primitives (button actions share `button`; cards, badges, chips and stats take no classes, so a site covers them
10
+ * from its skin stylesheet) join the lists; from 3.15.0 the proof sections (projects, gallery, before-after,
11
+ * licences, emergency) and the quote checklist.
9
12
  * - The component is covered when, on at least one sampled page, its root element or one of its slot elements (an
10
13
  * element with `data-slot` inside it, outside any component nested in it) carries a class, because a skin gives a
11
14
  * component its look through `classes`; or when the project's own stylesheets under `src/` or `app/` select the
@@ -21,9 +24,17 @@ import { PageProblems } from "../library/common.js";
21
24
  export const K2_SKINNED_HOOKS = [
22
25
  "header", "drawer", "utility", "callbar", "back-to-top", "quote-button", "phone-link", "text-link", "button", "container",
23
26
  "quote-form", "photo-upload", "dialog", "field", "thank-you", "rating-chip", "footer", "accordion", "price-table",
27
+ // 3.14.0 primitives
28
+ "eyebrow", "card", "badge", "chip", "stat", "tick-list", "link-list", "media", "breadcrumbs",
29
+ // 3.15.0
30
+ "quote-checklist",
24
31
  ];
25
32
  /** `data-bl-section` ids of the sections the library renders and skins. */
26
- export const K2_SKINNED_SECTIONS = ["reviews", "cta", "faqs", "price-guide", "proof-strip", "page-header", "contact"];
33
+ export const K2_SKINNED_SECTIONS = [
34
+ "reviews", "cta", "faqs", "price-guide", "proof-strip", "page-header", "contact", "hero", "services",
35
+ // 3.15.0 proof sections
36
+ "projects", "gallery", "before-after", "licences", "emergency",
37
+ ];
27
38
  export const K2_REFERENCE_CLASS = /^ref-(?:heavy|calm|pool)-/;
28
39
  function classesOf(element) {
29
40
  return (attr(element, "class") ?? "").split(/\s+/).filter(Boolean);
@@ -1 +1 @@
1
- {"version":3,"file":"k2-skin-coverage.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/k2-skin-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * K2: skin coverage (Site System v2 §13.2, §8.4). Every shared component a page renders has a skin entry, so a site\n * never ships an unstyled look-alike, and no reference-skin class (the test skins the library's own tests use) ships.\n *\n * What the HTML can show, defined exactly:\n * - A component is a rendered element with `data-bl=\"<name>\"` or `data-bl-section=\"<id>\"`, for the names the library\n * skins (SKINNED_HOOKS and SKINNED_SECTIONS below). Hooks without a skin entry (the skip link, the honeypot, a star\n * row) and the site's own compositions (the hero) are not components in this sense.\n * - The component is covered when, on at least one sampled page, its root element or one of its slot elements (an\n * element with `data-slot` inside it, outside any component nested in it) carries a class, because a skin gives a\n * component its look through `classes`; or when the project's own stylesheets under `src/` or `app/` select the\n * component by its hook (`[data-bl=\"footer\"]`, `[data-bl-section=\"faqs\"]`), which is how a site styles slots in plain CSS.\n * - A class token starting `ref-heavy-`, `ref-calm-` or `ref-pool-` on any element is a reference-skin class and fails.\n */\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { attr, descendants, type HtmlElement } from \"../enquiry/html.js\";\nimport { PageProblems } from \"../library/common.js\";\n\n/** `data-bl` names that have an entry in `BrandlabSkin` (ui-kit/brandlab-skin.ts). */\nexport const K2_SKINNED_HOOKS = [\n \"header\", \"drawer\", \"utility\", \"callbar\", \"back-to-top\", \"quote-button\", \"phone-link\", \"text-link\", \"button\", \"container\",\n \"quote-form\", \"photo-upload\", \"dialog\", \"field\", \"thank-you\", \"rating-chip\", \"footer\", \"accordion\", \"price-table\",\n] as const;\n\n/** `data-bl-section` ids of the sections the library renders and skins. */\nexport const K2_SKINNED_SECTIONS = [\"reviews\", \"cta\", \"faqs\", \"price-guide\", \"proof-strip\", \"page-header\", \"contact\"] as const;\n\nexport const K2_REFERENCE_CLASS = /^ref-(?:heavy|calm|pool)-/;\n\nfunction classesOf(element: HtmlElement): string[] {\n return (attr(element, \"class\") ?? \"\").split(/\\s+/).filter(Boolean);\n}\n\n/** The root and slot elements of one component, leaving out components nested inside it. */\nfunction skinSurface(root: HtmlElement): HtmlElement[] {\n const surface = [root];\n const visit = (node: HtmlElement) => {\n for (const child of node.children) {\n if (child.type !== \"element\") continue;\n if (attr(child, \"data-bl\") !== undefined || attr(child, \"data-bl-section\") !== undefined) continue;\n if (attr(child, \"data-slot\") !== undefined) surface.push(child);\n visit(child);\n }\n };\n visit(root);\n return surface;\n}\n\n/** Hook names the project's own stylesheets select: `[data-bl=\"x\"]` and `[data-bl-section=\"x\"]`. */\nexport function hooksStyledBySite(projectRoot: string): Set<string> {\n const styled = new Set<string>();\n const selector = /\\[\\s*data-bl(?:-section)?\\s*=\\s*[\"']?([a-z0-9-]+)[\"']?\\s*\\]/gi;\n const visit = (directory: string, depth: number) => {\n if (depth > 6) return;\n let entries;\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const path = join(directory, entry.name);\n if (entry.isDirectory()) visit(path, depth + 1);\n else if (entry.isFile() && /\\.(?:css|scss)$/i.test(entry.name)) {\n try {\n for (const match of readFileSync(path, \"utf8\").matchAll(selector)) styled.add(match[1]!.toLowerCase());\n } catch {\n // an unreadable stylesheet styles nothing we can see\n }\n }\n }\n };\n for (const folder of [\"src\", \"app\", \"sites\"]) if (existsSync(join(projectRoot, folder))) visit(join(projectRoot, folder), 0);\n return styled;\n}\n\nexport function checkK2(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n const hooks = new Set<string>(K2_SKINNED_HOOKS);\n const sections = new Set<string>(K2_SKINNED_SECTIONS);\n /** Component key (\"footer\", \"section:faqs\") to whether any instance carries a skin class, and the first page. */\n const components = new Map<string, { covered: boolean; page: string; label: string }>();\n const references = new PageProblems();\n\n for (const page of pages) {\n for (const element of descendants(page.root)) {\n const reference = classesOf(element).find((token) => K2_REFERENCE_CLASS.test(token));\n if (reference) references.add(page.path, `A reference-skin class (${reference}) ships; those belong to the library's own tests`);\n\n const hook = attr(element, \"data-bl\");\n const section = attr(element, \"data-bl-section\");\n const key = hook !== undefined && hooks.has(hook) ? `hook:${hook}` : section !== undefined && sections.has(section) ? `section:${section}` : null;\n if (!key) continue;\n const label = hook !== undefined ? `data-bl=\"${hook}\"` : `data-bl-section=\"${section}\"`;\n const carries = skinSurface(element).some((candidate) => classesOf(candidate).length > 0);\n const known = components.get(key);\n if (known) known.covered ||= carries;\n else components.set(key, { covered: carries, page: page.path, label });\n }\n }\n\n const failures = references.findings(\"K2\");\n if (components.size === 0 && failures.length === 0) return [notApplicable(\"K2\", \"None of the sampled pages renders a shared component.\")];\n const uncovered = [...components].filter(([, component]) => !component.covered);\n if (uncovered.length > 0) {\n const styled = hooksStyledBySite(projectRoot);\n for (const [key, component] of uncovered) {\n const name = key.slice(key.indexOf(\":\") + 1);\n if (styled.has(name)) continue;\n failures.push(fail(\"K2\", component.page, `${component.label} is rendered (first on ${component.page}) with no skin class on its root or slots and no selector for it in the site's stylesheets, so it has no skin entry.`));\n }\n }\n if (failures.length > 0) return failures;\n return [pass(\"K2\", `Checked ${plural(components.size, \"shared component\")} on ${plural(pages.length, \"page\")}: each carries a skin class or is styled by the site's own CSS, and no reference-skin class ships.`)];\n}\n"]}
1
+ {"version":3,"file":"k2-skin-coverage.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/k2-skin-coverage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * K2: skin coverage (Site System v2 §13.2, §8.4). Every shared component a page renders has a skin entry, so a site\n * never ships an unstyled look-alike, and no reference-skin class (the test skins the library's own tests use) ships.\n *\n * What the HTML can show, defined exactly:\n * - A component is a rendered element with `data-bl=\"<name>\"` or `data-bl-section=\"<id>\"`, for the names the library\n * skins (SKINNED_HOOKS and SKINNED_SECTIONS below). Hooks without a skin entry (the skip link, the honeypot, a star\n * row) and the site's own compositions are not components in this sense. From 3.14.0 the library's hero and the\n * primitives (button actions share `button`; cards, badges, chips and stats take no classes, so a site covers them\n * from its skin stylesheet) join the lists; from 3.15.0 the proof sections (projects, gallery, before-after,\n * licences, emergency) and the quote checklist.\n * - The component is covered when, on at least one sampled page, its root element or one of its slot elements (an\n * element with `data-slot` inside it, outside any component nested in it) carries a class, because a skin gives a\n * component its look through `classes`; or when the project's own stylesheets under `src/` or `app/` select the\n * component by its hook (`[data-bl=\"footer\"]`, `[data-bl-section=\"faqs\"]`), which is how a site styles slots in plain CSS.\n * - A class token starting `ref-heavy-`, `ref-calm-` or `ref-pool-` on any element is a reference-skin class and fails.\n */\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { attr, descendants, type HtmlElement } from \"../enquiry/html.js\";\nimport { PageProblems } from \"../library/common.js\";\n\n/** `data-bl` names that have an entry in `BrandlabSkin` (ui-kit/brandlab-skin.ts). */\nexport const K2_SKINNED_HOOKS = [\n \"header\", \"drawer\", \"utility\", \"callbar\", \"back-to-top\", \"quote-button\", \"phone-link\", \"text-link\", \"button\", \"container\",\n \"quote-form\", \"photo-upload\", \"dialog\", \"field\", \"thank-you\", \"rating-chip\", \"footer\", \"accordion\", \"price-table\",\n // 3.14.0 primitives\n \"eyebrow\", \"card\", \"badge\", \"chip\", \"stat\", \"tick-list\", \"link-list\", \"media\", \"breadcrumbs\",\n // 3.15.0\n \"quote-checklist\",\n] as const;\n\n/** `data-bl-section` ids of the sections the library renders and skins. */\nexport const K2_SKINNED_SECTIONS = [\n \"reviews\", \"cta\", \"faqs\", \"price-guide\", \"proof-strip\", \"page-header\", \"contact\", \"hero\", \"services\",\n // 3.15.0 proof sections\n \"projects\", \"gallery\", \"before-after\", \"licences\", \"emergency\",\n] as const;\n\nexport const K2_REFERENCE_CLASS = /^ref-(?:heavy|calm|pool)-/;\n\nfunction classesOf(element: HtmlElement): string[] {\n return (attr(element, \"class\") ?? \"\").split(/\\s+/).filter(Boolean);\n}\n\n/** The root and slot elements of one component, leaving out components nested inside it. */\nfunction skinSurface(root: HtmlElement): HtmlElement[] {\n const surface = [root];\n const visit = (node: HtmlElement) => {\n for (const child of node.children) {\n if (child.type !== \"element\") continue;\n if (attr(child, \"data-bl\") !== undefined || attr(child, \"data-bl-section\") !== undefined) continue;\n if (attr(child, \"data-slot\") !== undefined) surface.push(child);\n visit(child);\n }\n };\n visit(root);\n return surface;\n}\n\n/** Hook names the project's own stylesheets select: `[data-bl=\"x\"]` and `[data-bl-section=\"x\"]`. */\nexport function hooksStyledBySite(projectRoot: string): Set<string> {\n const styled = new Set<string>();\n const selector = /\\[\\s*data-bl(?:-section)?\\s*=\\s*[\"']?([a-z0-9-]+)[\"']?\\s*\\]/gi;\n const visit = (directory: string, depth: number) => {\n if (depth > 6) return;\n let entries;\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\") continue;\n const path = join(directory, entry.name);\n if (entry.isDirectory()) visit(path, depth + 1);\n else if (entry.isFile() && /\\.(?:css|scss)$/i.test(entry.name)) {\n try {\n for (const match of readFileSync(path, \"utf8\").matchAll(selector)) styled.add(match[1]!.toLowerCase());\n } catch {\n // an unreadable stylesheet styles nothing we can see\n }\n }\n }\n };\n for (const folder of [\"src\", \"app\", \"sites\"]) if (existsSync(join(projectRoot, folder))) visit(join(projectRoot, folder), 0);\n return styled;\n}\n\nexport function checkK2(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n const hooks = new Set<string>(K2_SKINNED_HOOKS);\n const sections = new Set<string>(K2_SKINNED_SECTIONS);\n /** Component key (\"footer\", \"section:faqs\") to whether any instance carries a skin class, and the first page. */\n const components = new Map<string, { covered: boolean; page: string; label: string }>();\n const references = new PageProblems();\n\n for (const page of pages) {\n for (const element of descendants(page.root)) {\n const reference = classesOf(element).find((token) => K2_REFERENCE_CLASS.test(token));\n if (reference) references.add(page.path, `A reference-skin class (${reference}) ships; those belong to the library's own tests`);\n\n const hook = attr(element, \"data-bl\");\n const section = attr(element, \"data-bl-section\");\n const key = hook !== undefined && hooks.has(hook) ? `hook:${hook}` : section !== undefined && sections.has(section) ? `section:${section}` : null;\n if (!key) continue;\n const label = hook !== undefined ? `data-bl=\"${hook}\"` : `data-bl-section=\"${section}\"`;\n const carries = skinSurface(element).some((candidate) => classesOf(candidate).length > 0);\n const known = components.get(key);\n if (known) known.covered ||= carries;\n else components.set(key, { covered: carries, page: page.path, label });\n }\n }\n\n const failures = references.findings(\"K2\");\n if (components.size === 0 && failures.length === 0) return [notApplicable(\"K2\", \"None of the sampled pages renders a shared component.\")];\n const uncovered = [...components].filter(([, component]) => !component.covered);\n if (uncovered.length > 0) {\n const styled = hooksStyledBySite(projectRoot);\n for (const [key, component] of uncovered) {\n const name = key.slice(key.indexOf(\":\") + 1);\n if (styled.has(name)) continue;\n failures.push(fail(\"K2\", component.page, `${component.label} is rendered (first on ${component.page}) with no skin class on its root or slots and no selector for it in the site's stylesheets, so it has no skin entry.`));\n }\n }\n if (failures.length > 0) return failures;\n return [pass(\"K2\", `Checked ${plural(components.size, \"shared component\")} on ${plural(pages.length, \"page\")}: each carries a skin class or is styled by the site's own CSS, and no reference-skin class ships.`)];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"t6-media-entries.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAK1F,eAAO,MAAM,aAAa,sBAAsB,CAAC;AACjD,eAAO,MAAM,cAAc,kDAAmD,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CA2BxG;AAOD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAgC9F"}
1
+ {"version":3,"file":"t6-media-entries.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAO1F,eAAO,MAAM,aAAa,sBAAsB,CAAC;AACjD,eAAO,MAAM,cAAc,kDAAmD,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,cAAc,GAAG,IAAI,CAaxG;AAyBD,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CA8C9F"}
@@ -8,13 +8,18 @@
8
8
  * photographs the business has to vouch for, so they need no entry.
9
9
  *
10
10
  * The manifest is read from the syntax tree, never run. A manifest the reader cannot resolve to plain data (a spread or
11
- * a computed value) reports nothing rather than guessing.
11
+ * a computed value, as AA's gallery entries) is read from its snapshot, `src/data/media.generated.json`, which the site
12
+ * writes with `writeMediaSnapshot` (`@brandlab-digital/conformance/media-snapshot`, 3.15.0); without one T6 fails and
13
+ * says how to record it, rather than guessing. A snapshot older than the files it hashed, or one that lists other
14
+ * images than a plain-data media.ts, fails as stale.
12
15
  */
13
16
  import { existsSync, readFileSync } from "node:fs";
14
17
  import { join } from "node:path";
15
18
  import { fail, notApplicable, pass, plural } from "../enquiry/common.js";
16
19
  import { LiteralReader, UNRESOLVED, isFullyResolved, isLiteralObject } from "../enquiry/literal.js";
17
20
  import { PageProblems, namedList } from "../library/common.js";
21
+ import { estatePlanSourceHash } from "../../../estate-snapshot.js";
22
+ import { MEDIA_SNAPSHOT_FILE, MEDIA_SNAPSHOT_KIND } from "../../../media-snapshot.js";
18
23
  import { MEDIA_EXEMPT_NAME, imagesIn, needsMediaEntry, publicImages } from "./images.js";
19
24
  export const T6_MEDIA_PATH = "src/data/media.ts";
20
25
  export const T6_PROVENANCES = ["genuine", "stock", "illustrative-ai"];
@@ -34,7 +39,10 @@ export function readMediaManifest(projectRoot) {
34
39
  const [argument] = reader.callArguments("defineMedia");
35
40
  if (!argument)
36
41
  return "not-declared";
37
- const value = reader.evaluate(argument);
42
+ return manifestOf(reader.evaluate(argument));
43
+ }
44
+ /** The entries of a manifest value, or null when they are not plain data. */
45
+ function manifestOf(value) {
38
46
  if (!isLiteralObject(value))
39
47
  return null;
40
48
  const sources = new Map();
@@ -66,7 +74,24 @@ export function checkT6(pages, projectRoot) {
66
74
  shown.set(image.file, [...(shown.get(image.file) ?? []), page]);
67
75
  }
68
76
  }
69
- const manifest = readMediaManifest(projectRoot);
77
+ const literal = readMediaManifest(projectRoot);
78
+ const snapshot = readMediaSnapshot(projectRoot);
79
+ let manifest = literal;
80
+ let read = T6_MEDIA_PATH;
81
+ if (snapshot !== null) {
82
+ if ("problem" in snapshot)
83
+ return [fail("T6", MEDIA_SNAPSHOT_FILE, snapshot.problem)];
84
+ if (snapshot.stale)
85
+ return [fail("T6", MEDIA_SNAPSHOT_FILE, snapshot.stale)];
86
+ if (typeof literal === "object" && literal !== null && !sameSources(literal.sources, snapshot.manifest.sources)) {
87
+ return [fail("T6", MEDIA_SNAPSHOT_FILE, `${MEDIA_SNAPSHOT_FILE} does not list the same images as ${T6_MEDIA_PATH}, so it is stale; ${REGENERATE}.`)];
88
+ }
89
+ manifest = snapshot.manifest;
90
+ read = MEDIA_SNAPSHOT_FILE;
91
+ }
92
+ else if (literal === null) {
93
+ return [fail("T6", T6_MEDIA_PATH, `${T6_MEDIA_PATH} is not plain data (a spread or a computed value), so its entries cannot be read; write the manifest as a literal, or record it with writeMediaSnapshot from @brandlab-digital/conformance/media-snapshot (${MEDIA_SNAPSHOT_FILE}).`)];
94
+ }
70
95
  if (manifest === null)
71
96
  return [];
72
97
  if (manifest === "missing" || manifest === "not-declared") {
@@ -77,7 +102,7 @@ export function checkT6(pages, projectRoot) {
77
102
  return [fail("T6", T6_MEDIA_PATH, `${reason}, but the site has ${count}; every image needs a media.ts entry with provenance.`)];
78
103
  }
79
104
  const sources = new Set(manifest.sources.values());
80
- const failures = manifest.entryProblems.map((problem) => fail("T6", T6_MEDIA_PATH, `${T6_MEDIA_PATH}: ${problem}.`));
105
+ const failures = manifest.entryProblems.map((problem) => fail("T6", read, `${read}: ${problem}.`));
81
106
  const unlisted = onDisk.filter((image) => !covered(sources, image.web)).map((image) => image.web);
82
107
  if (unlisted.length > 0) {
83
108
  failures.push(fail("T6", "public", `${plural(unlisted.length, "image")} in public/ ${unlisted.length === 1 ? "has" : "have"} no entry in ${T6_MEDIA_PATH}: ${namedList(unlisted)}.`));
@@ -91,6 +116,45 @@ export function checkT6(pages, projectRoot) {
91
116
  failures.push(...problems.findings("T6"));
92
117
  if (failures.length > 0)
93
118
  return failures;
94
- return [pass("T6", `Checked ${plural(onDisk.length, "image file")} in public/ and ${plural(shown.size, "image")} on the pages: each has an entry in ${T6_MEDIA_PATH} with provenance.`)];
119
+ return [pass("T6", `Checked ${plural(onDisk.length, "image file")} in public/ and ${plural(shown.size, "image")} on the pages: each has an entry in ${read} with provenance.`)];
120
+ }
121
+ const REGENERATE = "run the site's tests (writeMediaSnapshot) to write it again, and commit it";
122
+ function sameSources(left, right) {
123
+ return left.size === right.size && [...left].every(([id, src]) => right.get(id) === src);
124
+ }
125
+ /**
126
+ * The media snapshot (3.15.0), or null when the site has none. `stale` says why it no longer follows its sources: the
127
+ * files it hashed have changed since it was written.
128
+ */
129
+ function readMediaSnapshot(projectRoot) {
130
+ const file = join(projectRoot, MEDIA_SNAPSHOT_FILE);
131
+ if (!existsSync(file))
132
+ return null;
133
+ let value;
134
+ try {
135
+ value = JSON.parse(readFileSync(file, "utf8"));
136
+ }
137
+ catch {
138
+ return { problem: `${MEDIA_SNAPSHOT_FILE} is not valid JSON; ${REGENERATE}.` };
139
+ }
140
+ const record = value;
141
+ const sources = isLiteralObject(record) ? record["sources"] : undefined;
142
+ const manifest = isLiteralObject(record) ? manifestOf(record["media"] ?? null) : null;
143
+ if (!isLiteralObject(record) || record["kind"] !== MEDIA_SNAPSHOT_KIND || !isLiteralObject(sources) || typeof sources["sha256"] !== "string"
144
+ || !Array.isArray(sources["paths"]) || !sources["paths"].every((path) => typeof path === "string") || manifest === null) {
145
+ return { problem: `${MEDIA_SNAPSHOT_FILE} is not a media snapshot (no kind "${MEDIA_SNAPSHOT_KIND}" with its sources and entries); ${REGENERATE}.` };
146
+ }
147
+ const paths = sources["paths"];
148
+ let current;
149
+ try {
150
+ current = estatePlanSourceHash(projectRoot, paths);
151
+ }
152
+ catch (error) {
153
+ return { problem: `${MEDIA_SNAPSHOT_FILE}: ${error instanceof Error ? error.message : String(error)}` };
154
+ }
155
+ const stale = current === sources["sha256"]
156
+ ? null
157
+ : `${MEDIA_SNAPSHOT_FILE} is older than the files it was written from (${paths.join(", ")} changed since), so it may not match media.ts; ${REGENERATE}.`;
158
+ return { manifest, stale };
95
159
  }
96
160
  //# sourceMappingURL=t6-media-entries.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"t6-media-entries.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * T6: every image has a media.ts entry with provenance (Site System v2 §13.2, F13).\n *\n * `src/data/media.ts` declares the manifest with `defineMedia({ id: { src, width, height, alt, provenance, rights … } })`.\n * Every image file under `public/` and every image the built pages show (a `next/image` URL is read back to the file it\n * optimises) must be the `src` of an entry, and every entry names its provenance (`genuine`, `stock` or\n * `illustrative-ai`) and its rights. Logos, icons, SVG art and third-party profile photos (review avatars) are not\n * photographs the business has to vouch for, so they need no entry.\n *\n * The manifest is read from the syntax tree, never run. A manifest the reader cannot resolve to plain data (a spread or\n * a computed value) reports nothing rather than guessing.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { LiteralReader, UNRESOLVED, isFullyResolved, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\nimport { PageProblems, namedList } from \"../library/common.js\";\nimport { MEDIA_EXEMPT_NAME, imagesIn, needsMediaEntry, publicImages } from \"./images.js\";\n\nexport const T6_MEDIA_PATH = \"src/data/media.ts\";\nexport const T6_PROVENANCES = [\"genuine\", \"stock\", \"illustrative-ai\"] as const;\n\nexport interface MediaManifest {\n /** Media id to its `src`. */\n readonly sources: ReadonlyMap<string, string>;\n /** Problems with entries, one plain fragment each (\"media.hero has no provenance\"). */\n readonly entryProblems: readonly string[];\n}\n\n/** \"missing\": no media.ts; \"not-declared\": no defineMedia call in it; null: unreadable; else the manifest. */\nexport function readMediaManifest(projectRoot: string): MediaManifest | \"missing\" | \"not-declared\" | null {\n const file = join(projectRoot, T6_MEDIA_PATH);\n if (!existsSync(file)) return \"missing\";\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return null;\n }\n const reader = new LiteralReader(text, T6_MEDIA_PATH);\n const [argument] = reader.callArguments(\"defineMedia\");\n if (!argument) return \"not-declared\";\n const value: Literal = reader.evaluate(argument);\n if (!isLiteralObject(value)) return null;\n const sources = new Map<string, string>();\n const entryProblems: string[] = [];\n for (const [id, entry] of Object.entries(value)) {\n if (!isLiteralObject(entry) || !isFullyResolved(entry.src) || typeof entry.src !== \"string\") return null;\n sources.set(id, entry.src);\n if (!(T6_PROVENANCES as readonly unknown[]).includes(entry.provenance)) {\n entryProblems.push(`media.${id} has no provenance (genuine, stock or illustrative-ai)`);\n }\n if (typeof entry.rights !== \"string\" || entry.rights.trim() === \"\") {\n if (entry.rights !== UNRESOLVED) entryProblems.push(`media.${id} does not say who holds the rights`);\n }\n }\n return { sources, entryProblems };\n}\n\n/** The part of an https URL entry that matches a page: the whole URL. */\nfunction covered(sources: ReadonlySet<string>, file: string): boolean {\n return sources.has(file);\n}\n\nexport function checkT6(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n const onDisk = publicImages(projectRoot).filter((image) => !/\\.svg$/i.test(image.web) && !MEDIA_EXEMPT_NAME.test(image.web));\n const shown = new Map<string, ParsedPage[]>();\n for (const page of pages) {\n for (const image of imagesIn(page.root)) {\n if (needsMediaEntry(image)) shown.set(image.file, [...(shown.get(image.file) ?? []), page]);\n }\n }\n\n const manifest = readMediaManifest(projectRoot);\n if (manifest === null) return [];\n if (manifest === \"missing\" || manifest === \"not-declared\") {\n if (onDisk.length === 0 && shown.size === 0) return [notApplicable(\"T6\", \"The site has no images to record in a media manifest.\")];\n const reason = manifest === \"missing\" ? `${T6_MEDIA_PATH} does not exist` : `${T6_MEDIA_PATH} does not declare the manifest with defineMedia`;\n const count = onDisk.length > 0 ? plural(onDisk.length, \"image file\") + \" in public/\" : plural(shown.size, \"image\") + \" on the pages\";\n return [fail(\"T6\", T6_MEDIA_PATH, `${reason}, but the site has ${count}; every image needs a media.ts entry with provenance.`)];\n }\n\n const sources = new Set(manifest.sources.values());\n const failures: SiteSystemFinding[] = manifest.entryProblems.map((problem) => fail(\"T6\", T6_MEDIA_PATH, `${T6_MEDIA_PATH}: ${problem}.`));\n const unlisted = onDisk.filter((image) => !covered(sources, image.web)).map((image) => image.web);\n if (unlisted.length > 0) {\n failures.push(fail(\"T6\", \"public\", `${plural(unlisted.length, \"image\")} in public/ ${unlisted.length === 1 ? \"has\" : \"have\"} no entry in ${T6_MEDIA_PATH}: ${namedList(unlisted)}.`));\n }\n const problems = new PageProblems();\n for (const page of pages) {\n const missing = [...shown].filter(([file, owners]) => owners.includes(page) && !covered(sources, file)).map(([file]) => file);\n if (missing.length > 0) problems.add(page.path, `${plural(missing.length, \"image\")} shown with no entry in ${T6_MEDIA_PATH} (${namedList(missing, 3)})`);\n }\n failures.push(...problems.findings(\"T6\"));\n if (failures.length > 0) return failures;\n return [pass(\"T6\", `Checked ${plural(onDisk.length, \"image file\")} in public/ and ${plural(shown.size, \"image\")} on the pages: each has an entry in ${T6_MEDIA_PATH} with provenance.`)];\n}\n"]}
1
+ {"version":3,"file":"t6-media-entries.js","sourceRoot":"","sources":["../../../../src/site-system/rules/sections/t6-media-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAgB,MAAM,uBAAuB,CAAC;AAClH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtF,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzF,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAAC;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,iBAAiB,CAAU,CAAC;AAS/E,8GAA8G;AAC9G,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IACvD,IAAI,CAAC,QAAQ;QAAE,OAAO,cAAc,CAAC;IACrC,OAAO,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,6EAA6E;AAC7E,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACzG,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAE,cAAqC,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YACvE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,wDAAwD,CAAC,CAAC;QAC1F,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnE,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU;gBAAE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,oCAAoC,CAAC,CAAC;QACvG,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,SAAS,OAAO,CAAC,OAA4B,EAAE,IAAY;IACzD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAA4B,EAAE,WAAmB;IACvE,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7H,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,IAAI,eAAe,CAAC,KAAK,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAChD,IAAI,QAAQ,GAAG,OAAO,CAAC;IACvB,IAAI,IAAI,GAAG,aAAa,CAAC;IACzB,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,IAAI,SAAS,IAAI,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QACtF,IAAI,QAAQ,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7E,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAChH,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,EAAE,GAAG,mBAAmB,qCAAqC,aAAa,qBAAqB,UAAU,GAAG,CAAC,CAAC,CAAC;QACvJ,CAAC;QACD,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC7B,IAAI,GAAG,mBAAmB,CAAC;IAC7B,CAAC;SAAM,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,aAAa,8MAA8M,mBAAmB,IAAI,CAAC,CAAC,CAAC;IAC5R,CAAC;IACD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;QAC1D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,uDAAuD,CAAC,CAAC,CAAC;QACnI,MAAM,MAAM,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,aAAa,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,iDAAiD,CAAC;QAC9I,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,eAAe,CAAC;QACtI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,sBAAsB,KAAK,uDAAuD,CAAC,CAAC,CAAC;IAClI,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAwB,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC;IACxH,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClG,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,eAAe,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,gBAAgB,aAAa,KAAK,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACxL,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QAC9H,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,2BAA2B,aAAa,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3J,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,mBAAmB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,uCAAuC,IAAI,mBAAmB,CAAC,CAAC,CAAC;AAClL,CAAC;AAED,MAAM,UAAU,GAAG,4EAA4E,CAAC;AAEhG,SAAS,WAAW,CAAC,IAAiC,EAAE,KAAkC;IACxF,OAAO,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC;AAC3F,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,WAAmB;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAC;IACpD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,GAAG,mBAAmB,uBAAuB,UAAU,GAAG,EAAE,CAAC;IACjF,CAAC;IACD,MAAM,MAAM,GAAG,KAAmC,CAAC;IACnD,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtF,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,mBAAmB,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,QAAQ,CAAC,KAAK,QAAQ;WACvI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC1H,OAAO,EAAE,OAAO,EAAE,GAAG,mBAAmB,sCAAsC,mBAAmB,oCAAoC,UAAU,GAAG,EAAE,CAAC;IACvJ,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAa,CAAC;IAC3C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,oBAAoB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,GAAG,mBAAmB,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IAC1G,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC;QACzC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,GAAG,mBAAmB,iDAAiD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kDAAkD,UAAU,GAAG,CAAC;IAC3J,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["/**\n * T6: every image has a media.ts entry with provenance (Site System v2 §13.2, F13).\n *\n * `src/data/media.ts` declares the manifest with `defineMedia({ id: { src, width, height, alt, provenance, rights … } })`.\n * Every image file under `public/` and every image the built pages show (a `next/image` URL is read back to the file it\n * optimises) must be the `src` of an entry, and every entry names its provenance (`genuine`, `stock` or\n * `illustrative-ai`) and its rights. Logos, icons, SVG art and third-party profile photos (review avatars) are not\n * photographs the business has to vouch for, so they need no entry.\n *\n * The manifest is read from the syntax tree, never run. A manifest the reader cannot resolve to plain data (a spread or\n * a computed value, as AA's gallery entries) is read from its snapshot, `src/data/media.generated.json`, which the site\n * writes with `writeMediaSnapshot` (`@brandlab-digital/conformance/media-snapshot`, 3.15.0); without one T6 fails and\n * says how to record it, rather than guessing. A snapshot older than the files it hashed, or one that lists other\n * images than a plain-data media.ts, fails as stale.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, pass, plural, type ParsedPage } from \"../enquiry/common.js\";\nimport { LiteralReader, UNRESOLVED, isFullyResolved, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\nimport { PageProblems, namedList } from \"../library/common.js\";\nimport { estatePlanSourceHash } from \"../../../estate-snapshot.js\";\nimport { MEDIA_SNAPSHOT_FILE, MEDIA_SNAPSHOT_KIND } from \"../../../media-snapshot.js\";\nimport { MEDIA_EXEMPT_NAME, imagesIn, needsMediaEntry, publicImages } from \"./images.js\";\n\nexport const T6_MEDIA_PATH = \"src/data/media.ts\";\nexport const T6_PROVENANCES = [\"genuine\", \"stock\", \"illustrative-ai\"] as const;\n\nexport interface MediaManifest {\n /** Media id to its `src`. */\n readonly sources: ReadonlyMap<string, string>;\n /** Problems with entries, one plain fragment each (\"media.hero has no provenance\"). */\n readonly entryProblems: readonly string[];\n}\n\n/** \"missing\": no media.ts; \"not-declared\": no defineMedia call in it; null: unreadable; else the manifest. */\nexport function readMediaManifest(projectRoot: string): MediaManifest | \"missing\" | \"not-declared\" | null {\n const file = join(projectRoot, T6_MEDIA_PATH);\n if (!existsSync(file)) return \"missing\";\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return null;\n }\n const reader = new LiteralReader(text, T6_MEDIA_PATH);\n const [argument] = reader.callArguments(\"defineMedia\");\n if (!argument) return \"not-declared\";\n return manifestOf(reader.evaluate(argument));\n}\n\n/** The entries of a manifest value, or null when they are not plain data. */\nfunction manifestOf(value: Literal): MediaManifest | null {\n if (!isLiteralObject(value)) return null;\n const sources = new Map<string, string>();\n const entryProblems: string[] = [];\n for (const [id, entry] of Object.entries(value)) {\n if (!isLiteralObject(entry) || !isFullyResolved(entry.src) || typeof entry.src !== \"string\") return null;\n sources.set(id, entry.src);\n if (!(T6_PROVENANCES as readonly unknown[]).includes(entry.provenance)) {\n entryProblems.push(`media.${id} has no provenance (genuine, stock or illustrative-ai)`);\n }\n if (typeof entry.rights !== \"string\" || entry.rights.trim() === \"\") {\n if (entry.rights !== UNRESOLVED) entryProblems.push(`media.${id} does not say who holds the rights`);\n }\n }\n return { sources, entryProblems };\n}\n\n/** The part of an https URL entry that matches a page: the whole URL. */\nfunction covered(sources: ReadonlySet<string>, file: string): boolean {\n return sources.has(file);\n}\n\nexport function checkT6(pages: readonly ParsedPage[], projectRoot: string): SiteSystemFinding[] {\n const onDisk = publicImages(projectRoot).filter((image) => !/\\.svg$/i.test(image.web) && !MEDIA_EXEMPT_NAME.test(image.web));\n const shown = new Map<string, ParsedPage[]>();\n for (const page of pages) {\n for (const image of imagesIn(page.root)) {\n if (needsMediaEntry(image)) shown.set(image.file, [...(shown.get(image.file) ?? []), page]);\n }\n }\n\n const literal = readMediaManifest(projectRoot);\n const snapshot = readMediaSnapshot(projectRoot);\n let manifest = literal;\n let read = T6_MEDIA_PATH;\n if (snapshot !== null) {\n if (\"problem\" in snapshot) return [fail(\"T6\", MEDIA_SNAPSHOT_FILE, snapshot.problem)];\n if (snapshot.stale) return [fail(\"T6\", MEDIA_SNAPSHOT_FILE, snapshot.stale)];\n if (typeof literal === \"object\" && literal !== null && !sameSources(literal.sources, snapshot.manifest.sources)) {\n return [fail(\"T6\", MEDIA_SNAPSHOT_FILE, `${MEDIA_SNAPSHOT_FILE} does not list the same images as ${T6_MEDIA_PATH}, so it is stale; ${REGENERATE}.`)];\n }\n manifest = snapshot.manifest;\n read = MEDIA_SNAPSHOT_FILE;\n } else if (literal === null) {\n return [fail(\"T6\", T6_MEDIA_PATH, `${T6_MEDIA_PATH} is not plain data (a spread or a computed value), so its entries cannot be read; write the manifest as a literal, or record it with writeMediaSnapshot from @brandlab-digital/conformance/media-snapshot (${MEDIA_SNAPSHOT_FILE}).`)];\n }\n if (manifest === null) return [];\n if (manifest === \"missing\" || manifest === \"not-declared\") {\n if (onDisk.length === 0 && shown.size === 0) return [notApplicable(\"T6\", \"The site has no images to record in a media manifest.\")];\n const reason = manifest === \"missing\" ? `${T6_MEDIA_PATH} does not exist` : `${T6_MEDIA_PATH} does not declare the manifest with defineMedia`;\n const count = onDisk.length > 0 ? plural(onDisk.length, \"image file\") + \" in public/\" : plural(shown.size, \"image\") + \" on the pages\";\n return [fail(\"T6\", T6_MEDIA_PATH, `${reason}, but the site has ${count}; every image needs a media.ts entry with provenance.`)];\n }\n\n const sources = new Set(manifest.sources.values());\n const failures: SiteSystemFinding[] = manifest.entryProblems.map((problem) => fail(\"T6\", read, `${read}: ${problem}.`));\n const unlisted = onDisk.filter((image) => !covered(sources, image.web)).map((image) => image.web);\n if (unlisted.length > 0) {\n failures.push(fail(\"T6\", \"public\", `${plural(unlisted.length, \"image\")} in public/ ${unlisted.length === 1 ? \"has\" : \"have\"} no entry in ${T6_MEDIA_PATH}: ${namedList(unlisted)}.`));\n }\n const problems = new PageProblems();\n for (const page of pages) {\n const missing = [...shown].filter(([file, owners]) => owners.includes(page) && !covered(sources, file)).map(([file]) => file);\n if (missing.length > 0) problems.add(page.path, `${plural(missing.length, \"image\")} shown with no entry in ${T6_MEDIA_PATH} (${namedList(missing, 3)})`);\n }\n failures.push(...problems.findings(\"T6\"));\n if (failures.length > 0) return failures;\n return [pass(\"T6\", `Checked ${plural(onDisk.length, \"image file\")} in public/ and ${plural(shown.size, \"image\")} on the pages: each has an entry in ${read} with provenance.`)];\n}\n\nconst REGENERATE = \"run the site's tests (writeMediaSnapshot) to write it again, and commit it\";\n\nfunction sameSources(left: ReadonlyMap<string, string>, right: ReadonlyMap<string, string>): boolean {\n return left.size === right.size && [...left].every(([id, src]) => right.get(id) === src);\n}\n\n/**\n * The media snapshot (3.15.0), or null when the site has none. `stale` says why it no longer follows its sources: the\n * files it hashed have changed since it was written.\n */\nfunction readMediaSnapshot(projectRoot: string): { manifest: MediaManifest; stale: string | null } | { problem: string } | null {\n const file = join(projectRoot, MEDIA_SNAPSHOT_FILE);\n if (!existsSync(file)) return null;\n let value: unknown;\n try {\n value = JSON.parse(readFileSync(file, \"utf8\"));\n } catch {\n return { problem: `${MEDIA_SNAPSHOT_FILE} is not valid JSON; ${REGENERATE}.` };\n }\n const record = value as { [key: string]: Literal };\n const sources = isLiteralObject(record) ? record[\"sources\"] : undefined;\n const manifest = isLiteralObject(record) ? manifestOf(record[\"media\"] ?? null) : null;\n if (!isLiteralObject(record) || record[\"kind\"] !== MEDIA_SNAPSHOT_KIND || !isLiteralObject(sources) || typeof sources[\"sha256\"] !== \"string\"\n || !Array.isArray(sources[\"paths\"]) || !sources[\"paths\"].every((path) => typeof path === \"string\") || manifest === null) {\n return { problem: `${MEDIA_SNAPSHOT_FILE} is not a media snapshot (no kind \"${MEDIA_SNAPSHOT_KIND}\" with its sources and entries); ${REGENERATE}.` };\n }\n const paths = sources[\"paths\"] as string[];\n let current: string;\n try {\n current = estatePlanSourceHash(projectRoot, paths);\n } catch (error) {\n return { problem: `${MEDIA_SNAPSHOT_FILE}: ${error instanceof Error ? error.message : String(error)}` };\n }\n const stale = current === sources[\"sha256\"]\n ? null\n : `${MEDIA_SNAPSHOT_FILE} is older than the files it was written from (${paths.join(\", \")} changed since), so it may not match media.ts; ${REGENERATE}.`;\n return { manifest, stale };\n}\n"]}
@@ -40,7 +40,7 @@ export function isSiteSystemFinding(value) {
40
40
  const finding = value;
41
41
  return (typeof finding.checkId === "string" &&
42
42
  KNOWN_IDS.has(finding.checkId) &&
43
- (finding.status === "pass" || finding.status === "fail" || finding.status === "not-applicable") &&
43
+ (finding.status === "pass" || finding.status === "fail" || finding.status === "not-run" || finding.status === "not-applicable") &&
44
44
  typeof finding.message === "string" &&
45
45
  (finding.target === undefined || typeof finding.target === "string") &&
46
46
  (finding.severity === undefined || finding.severity === "warning"));
@@ -1 +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;AAwBzE,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 /** The production build directory (`.next` unless `--build-dir` says otherwise), for rules that read the whole build. */\n readonly buildDir?: string;\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"]}
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;AAwBzE,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,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,gBAAgB,CAAC;QAC/H,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 /** The production build directory (`.next` unless `--build-dir` says otherwise), for rules that read the whole build. */\n readonly buildDir?: string;\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-run\" || 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"]}
@@ -1,5 +1,5 @@
1
1
  import { type BrandlabSiteSystemTestOptions } from './site-system/journeys/plan.js';
2
- export { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_DESKTOP_WIDTHS, SITE_SYSTEM_JOURNEY_IDS, SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_QUOTE_NAME, planBrandlabSiteSystemJourneys, siteSystemJourneyTitle, siteSystemJourneysFor, type BrandlabSiteSystemTestOptions, type SiteSystemActionOrder, type SiteSystemJourneyId, type SiteSystemJourneyPlan, } from './site-system/journeys/plan.js';
2
+ export { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_DESKTOP_WIDTHS, SITE_SYSTEM_JOURNEY_IDS, SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_QUOTE_NAME, planBrandlabSiteSystemJourneys, siteSystemJourneyRelease, siteSystemJourneyTitle, siteSystemJourneysFor, type BrandlabSiteSystemTestOptions, type SiteSystemActionOrder, type SiteSystemJourneyId, type SiteSystemJourneyPlan, } from './site-system/journeys/plan.js';
3
3
  export { HOVER_OPEN_WINDOW_MS, MOUSE_OUT_CLOSE_WINDOW_MS } from './site-system/journeys/menu.js';
4
4
  export { SITE_SYSTEM_PHONE_PLACEMENTS } from './site-system/journeys/page-helpers.js';
5
5
  export { W9_CACHE_HEADERS, W9_TRACKING_QUERY, W9_UNKNOWN_SLUG, notFoundVariantProblems, trackingVariantProblems, urlVariants, type W9Response, type W9Variants, } from './site-system/journeys/variants.js';
@@ -1 +1 @@
1
- {"version":3,"file":"site-system-e2e.d.ts","sourceRoot":"","sources":["../src/site-system-e2e.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAkC,KAAK,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAKpH,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,uBAAuB,EACvB,0BAA0B,EAC1B,sBAAsB,EACtB,8BAA8B,EAC9B,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACjG,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,WAAW,EACX,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,oCAAoC,CAAC;AAI5C,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAuB1F"}
1
+ {"version":3,"file":"site-system-e2e.d.ts","sourceRoot":"","sources":["../src/site-system-e2e.ts"],"names":[],"mappings":"AAgCA,OAAO,EAA4D,KAAK,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAK9I,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,uBAAuB,EACvB,0BAA0B,EAC1B,sBAAsB,EACtB,8BAA8B,EAC9B,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACjG,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,WAAW,EACX,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,oCAAoC,CAAC;AAa5C,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAuB1F"}
@@ -15,28 +15,41 @@
15
15
  *
16
16
  * The journeys find the header parts by role, so they test a bespoke header and the shared one alike: the header is
17
17
  * the banner landmark, the menu the navigation named "Main", each submenu a `button[aria-expanded][aria-controls]`
18
- * and its panel the element that names. The tier decides which journeys register (C1 from `enquiry`). They run in
18
+ * and its panel the element that names. The tier decides which journeys register (C1 from `enquiry`, W9 at `full`
19
+ * with `pages.families`), each from the release that brings it: the site's installed Brandlab release (a canary
20
+ * included), read from its package.json as brandlab-ready reads it, so every journey the gate blocks on runs. They run in
19
21
  * Chromium; K5 (focus not hidden) runs in every browser project the site's Playwright config defines, so add a
20
22
  * WebKit project to cover Safari. axe comes from `@axe-core/playwright` when the site has it, else `axe-core`.
21
23
  * Every request leaves the browser only as a GET or HEAD to the build under test.
22
24
  */
25
+ import { readFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
23
27
  import { test } from '@playwright/test';
24
28
  import { isolateFromNetwork } from './e2e-support.js';
25
29
  import { defineAccessJourneys } from './site-system/journeys/access.js';
26
30
  import { installPageHelpers } from './site-system/journeys/browser.js';
27
31
  import { defineConversionJourneys } from './site-system/journeys/conversion.js';
28
32
  import { defineMenuJourneys } from './site-system/journeys/menu.js';
29
- import { planBrandlabSiteSystemJourneys } from './site-system/journeys/plan.js';
33
+ import { planBrandlabSiteSystemJourneys, siteSystemJourneyRelease } from './site-system/journeys/plan.js';
30
34
  import { definePrefetchJourneys } from './site-system/journeys/prefetch.js';
31
35
  import { defineShellJourneys } from './site-system/journeys/shell.js';
32
36
  import { defineVariantJourneys } from './site-system/journeys/variants.js';
33
- export { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_DESKTOP_WIDTHS, SITE_SYSTEM_JOURNEY_IDS, SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_QUOTE_NAME, planBrandlabSiteSystemJourneys, siteSystemJourneyTitle, siteSystemJourneysFor, } from './site-system/journeys/plan.js';
37
+ export { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_DESKTOP_WIDTHS, SITE_SYSTEM_JOURNEY_IDS, SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_QUOTE_NAME, planBrandlabSiteSystemJourneys, siteSystemJourneyRelease, siteSystemJourneyTitle, siteSystemJourneysFor, } from './site-system/journeys/plan.js';
34
38
  export { HOVER_OPEN_WINDOW_MS, MOUSE_OUT_CLOSE_WINDOW_MS } from './site-system/journeys/menu.js';
35
39
  export { SITE_SYSTEM_PHONE_PLACEMENTS } from './site-system/journeys/page-helpers.js';
36
40
  export { W9_CACHE_HEADERS, W9_TRACKING_QUERY, W9_UNKNOWN_SLUG, notFoundVariantProblems, trackingVariantProblems, urlVariants, } from './site-system/journeys/variants.js';
37
41
  const CHROMIUM_ONLY = 'The site-system journeys run in Chromium; K5 runs in every browser project.';
42
+ /** The site's package.json (Playwright runs from the project root), or null when it cannot be read. */
43
+ function sitePackageJson() {
44
+ try {
45
+ return JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'));
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
38
51
  export function defineBrandlabSiteSystemTests(options) {
39
- const plan = planBrandlabSiteSystemJourneys(options);
52
+ const plan = planBrandlabSiteSystemJourneys({ ...options, release: options.release ?? siteSystemJourneyRelease(sitePackageJson()) });
40
53
  const runs = new Set(plan.journeys);
41
54
  const chromiumOnly = () => test.skip(({ browserName }) => browserName !== 'chromium', CHROMIUM_ONLY);
42
55
  test.describe('Brandlab site system', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"site-system-e2e.js","sourceRoot":"","sources":["../src/site-system-e2e.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,8BAA8B,EAAsC,MAAM,gCAAgC,CAAC;AACpH,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAE3E,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,uBAAuB,EACvB,0BAA0B,EAC1B,sBAAsB,EACtB,8BAA8B,EAC9B,sBAAsB,EACtB,qBAAqB,GAKtB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACjG,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,WAAW,GAGZ,MAAM,oCAAoC,CAAC;AAE5C,MAAM,aAAa,GAAG,6EAA6E,CAAC;AAEpG,MAAM,UAAU,6BAA6B,CAAC,OAAsC;IAClF,MAAM,IAAI,GAAG,8BAA8B,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,WAAW,KAAK,UAAU,EAAE,aAAa,CAAC,CAAC;IAErG,IAAI,CAAC,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACzC,0EAA0E;QAC1E,IAAI,CAAC,GAAG,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YAC1C,MAAM,kBAAkB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/D,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;YACtC,YAAY,EAAE,CAAC;YACf,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC/B,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAChC,sBAAsB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACnC,wBAAwB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACrC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QACH,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * The Site System v2 browser journeys for a Brandlab production build (release 3.8.0): the header and its menu,\n * the phone drawer and call bar, contrast, targets, focus, the action order, the money-page first screen and phone\n * placements, from 3.13.0 prefetch by link role (W2) and (full tier) the URL variants of each page family (W9). Every\n * test title starts with its ready-gate check id in square brackets (\"[N3] ...\") so `brandlab-ready` can read the\n * Playwright JSON report.\n *\n * // tests/e2e/site-system.spec.ts\n * import { defineBrandlabSiteSystemTests } from '@brandlab-digital/conformance/site-system-e2e'\n *\n * defineBrandlabSiteSystemTests({\n * tier: 'enquiry',\n * pages: { home: '/', money: ['/services/blocked-drains'], contact: '/contact' },\n * })\n *\n * The journeys find the header parts by role, so they test a bespoke header and the shared one alike: the header is\n * the banner landmark, the menu the navigation named \"Main\", each submenu a `button[aria-expanded][aria-controls]`\n * and its panel the element that names. The tier decides which journeys register (C1 from `enquiry`). They run in\n * Chromium; K5 (focus not hidden) runs in every browser project the site's Playwright config defines, so add a\n * WebKit project to cover Safari. axe comes from `@axe-core/playwright` when the site has it, else `axe-core`.\n * Every request leaves the browser only as a GET or HEAD to the build under test.\n */\nimport { test } from '@playwright/test';\nimport { isolateFromNetwork } from './e2e-support.js';\nimport { defineAccessJourneys } from './site-system/journeys/access.js';\nimport { installPageHelpers } from './site-system/journeys/browser.js';\nimport { defineConversionJourneys } from './site-system/journeys/conversion.js';\nimport { defineMenuJourneys } from './site-system/journeys/menu.js';\nimport { planBrandlabSiteSystemJourneys, type BrandlabSiteSystemTestOptions } from './site-system/journeys/plan.js';\nimport { definePrefetchJourneys } from './site-system/journeys/prefetch.js';\nimport { defineShellJourneys } from './site-system/journeys/shell.js';\nimport { defineVariantJourneys } from './site-system/journeys/variants.js';\n\nexport {\n SITE_SYSTEM_DESKTOP_VIEWPORT,\n SITE_SYSTEM_DESKTOP_WIDTHS,\n SITE_SYSTEM_JOURNEY_IDS,\n SITE_SYSTEM_PHONE_VIEWPORT,\n SITE_SYSTEM_QUOTE_NAME,\n planBrandlabSiteSystemJourneys,\n siteSystemJourneyTitle,\n siteSystemJourneysFor,\n type BrandlabSiteSystemTestOptions,\n type SiteSystemActionOrder,\n type SiteSystemJourneyId,\n type SiteSystemJourneyPlan,\n} from './site-system/journeys/plan.js';\nexport { HOVER_OPEN_WINDOW_MS, MOUSE_OUT_CLOSE_WINDOW_MS } from './site-system/journeys/menu.js';\nexport { SITE_SYSTEM_PHONE_PLACEMENTS } from './site-system/journeys/page-helpers.js';\nexport {\n W9_CACHE_HEADERS,\n W9_TRACKING_QUERY,\n W9_UNKNOWN_SLUG,\n notFoundVariantProblems,\n trackingVariantProblems,\n urlVariants,\n type W9Response,\n type W9Variants,\n} from './site-system/journeys/variants.js';\n\nconst CHROMIUM_ONLY = 'The site-system journeys run in Chromium; K5 runs in every browser project.';\n\nexport function defineBrandlabSiteSystemTests(options: BrandlabSiteSystemTestOptions): void {\n const plan = planBrandlabSiteSystemJourneys(options);\n const runs = new Set<string>(plan.journeys);\n const chromiumOnly = () => test.skip(({ browserName }) => browserName !== 'chromium', CHROMIUM_ONLY);\n\n test.describe('Brandlab site system', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n await installPageHelpers(page);\n });\n\n test.describe('Chromium journeys', () => {\n chromiumOnly();\n defineMenuJourneys(plan, runs);\n defineShellJourneys(plan, runs);\n definePrefetchJourneys(plan, runs);\n defineConversionJourneys(plan, runs);\n defineVariantJourneys(plan, runs);\n });\n defineAccessJourneys(plan, runs, { chromiumOnly });\n });\n}\n"]}
1
+ {"version":3,"file":"site-system-e2e.js","sourceRoot":"","sources":["../src/site-system-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Site System v2 browser journeys for a Brandlab production build (release 3.8.0): the header and its menu,\n * the phone drawer and call bar, contrast, targets, focus, the action order, the money-page first screen and phone\n * placements, from 3.13.0 prefetch by link role (W2) and (full tier) the URL variants of each page family (W9). Every\n * test title starts with its ready-gate check id in square brackets (\"[N3] ...\") so `brandlab-ready` can read the\n * Playwright JSON report.\n *\n * // tests/e2e/site-system.spec.ts\n * import { defineBrandlabSiteSystemTests } from '@brandlab-digital/conformance/site-system-e2e'\n *\n * defineBrandlabSiteSystemTests({\n * tier: 'enquiry',\n * pages: { home: '/', money: ['/services/blocked-drains'], contact: '/contact' },\n * })\n *\n * The journeys find the header parts by role, so they test a bespoke header and the shared one alike: the header is\n * the banner landmark, the menu the navigation named \"Main\", each submenu a `button[aria-expanded][aria-controls]`\n * and its panel the element that names. The tier decides which journeys register (C1 from `enquiry`, W9 at `full`\n * with `pages.families`), each from the release that brings it: the site's installed Brandlab release (a canary\n * included), read from its package.json as brandlab-ready reads it, so every journey the gate blocks on runs. They run in\n * Chromium; K5 (focus not hidden) runs in every browser project the site's Playwright config defines, so add a\n * WebKit project to cover Safari. axe comes from `@axe-core/playwright` when the site has it, else `axe-core`.\n * Every request leaves the browser only as a GET or HEAD to the build under test.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { test } from '@playwright/test';\nimport { isolateFromNetwork } from './e2e-support.js';\nimport { defineAccessJourneys } from './site-system/journeys/access.js';\nimport { installPageHelpers } from './site-system/journeys/browser.js';\nimport { defineConversionJourneys } from './site-system/journeys/conversion.js';\nimport { defineMenuJourneys } from './site-system/journeys/menu.js';\nimport { planBrandlabSiteSystemJourneys, siteSystemJourneyRelease, type BrandlabSiteSystemTestOptions } from './site-system/journeys/plan.js';\nimport { definePrefetchJourneys } from './site-system/journeys/prefetch.js';\nimport { defineShellJourneys } from './site-system/journeys/shell.js';\nimport { defineVariantJourneys } from './site-system/journeys/variants.js';\n\nexport {\n SITE_SYSTEM_DESKTOP_VIEWPORT,\n SITE_SYSTEM_DESKTOP_WIDTHS,\n SITE_SYSTEM_JOURNEY_IDS,\n SITE_SYSTEM_PHONE_VIEWPORT,\n SITE_SYSTEM_QUOTE_NAME,\n planBrandlabSiteSystemJourneys,\n siteSystemJourneyRelease,\n siteSystemJourneyTitle,\n siteSystemJourneysFor,\n type BrandlabSiteSystemTestOptions,\n type SiteSystemActionOrder,\n type SiteSystemJourneyId,\n type SiteSystemJourneyPlan,\n} from './site-system/journeys/plan.js';\nexport { HOVER_OPEN_WINDOW_MS, MOUSE_OUT_CLOSE_WINDOW_MS } from './site-system/journeys/menu.js';\nexport { SITE_SYSTEM_PHONE_PLACEMENTS } from './site-system/journeys/page-helpers.js';\nexport {\n W9_CACHE_HEADERS,\n W9_TRACKING_QUERY,\n W9_UNKNOWN_SLUG,\n notFoundVariantProblems,\n trackingVariantProblems,\n urlVariants,\n type W9Response,\n type W9Variants,\n} from './site-system/journeys/variants.js';\n\nconst CHROMIUM_ONLY = 'The site-system journeys run in Chromium; K5 runs in every browser project.';\n\n/** The site's package.json (Playwright runs from the project root), or null when it cannot be read. */\nfunction sitePackageJson(): Parameters<typeof siteSystemJourneyRelease>[0] {\n try {\n return JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8')) as Parameters<typeof siteSystemJourneyRelease>[0];\n } catch {\n return null;\n }\n}\n\nexport function defineBrandlabSiteSystemTests(options: BrandlabSiteSystemTestOptions): void {\n const plan = planBrandlabSiteSystemJourneys({ ...options, release: options.release ?? siteSystemJourneyRelease(sitePackageJson()) });\n const runs = new Set<string>(plan.journeys);\n const chromiumOnly = () => test.skip(({ browserName }) => browserName !== 'chromium', CHROMIUM_ONLY);\n\n test.describe('Brandlab site system', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n await installPageHelpers(page);\n });\n\n test.describe('Chromium journeys', () => {\n chromiumOnly();\n defineMenuJourneys(plan, runs);\n defineShellJourneys(plan, runs);\n definePrefetchJourneys(plan, runs);\n defineConversionJourneys(plan, runs);\n defineVariantJourneys(plan, runs);\n });\n defineAccessJourneys(plan, runs, { chromiumOnly });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/conformance",
3
- "version": "3.13.0",
3
+ "version": "3.15.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  "typecheck": "tsc --noEmit -p tsconfig.json"
30
30
  },
31
31
  "dependencies": {
32
- "@brandlab-digital/site-core": "3.13.0",
32
+ "@brandlab-digital/site-core": "3.15.0",
33
33
  "typescript": "6.0.3"
34
34
  },
35
35
  "peerDependencies": {
@@ -81,6 +81,14 @@
81
81
  "types": "./dist/estate.d.ts",
82
82
  "import": "./dist/estate.js"
83
83
  },
84
+ "./estate-snapshot": {
85
+ "types": "./dist/estate-snapshot.d.ts",
86
+ "import": "./dist/estate-snapshot.js"
87
+ },
88
+ "./media-snapshot": {
89
+ "types": "./dist/media-snapshot.d.ts",
90
+ "import": "./dist/media-snapshot.js"
91
+ },
84
92
  "./contact-e2e": {
85
93
  "types": "./dist/contact-e2e.d.ts",
86
94
  "import": "./dist/contact-e2e.js"