@brandlab-digital/conformance 3.12.1 → 3.14.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 (149) hide show
  1. package/README.md +41 -1
  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/edge-probe-cli.d.ts +57 -0
  12. package/dist/edge-probe-cli.d.ts.map +1 -0
  13. package/dist/edge-probe-cli.js +134 -0
  14. package/dist/edge-probe-cli.js.map +1 -0
  15. package/dist/engineering.d.ts +4 -1
  16. package/dist/engineering.d.ts.map +1 -1
  17. package/dist/engineering.js +19 -14
  18. package/dist/engineering.js.map +1 -1
  19. package/dist/estate-sample.d.ts +60 -0
  20. package/dist/estate-sample.d.ts.map +1 -0
  21. package/dist/estate-sample.js +144 -0
  22. package/dist/estate-sample.js.map +1 -0
  23. package/dist/estate.d.ts +45 -0
  24. package/dist/estate.d.ts.map +1 -1
  25. package/dist/estate.js +82 -22
  26. package/dist/estate.js.map +1 -1
  27. package/dist/origin-region.d.ts +14 -0
  28. package/dist/origin-region.d.ts.map +1 -0
  29. package/dist/origin-region.js +46 -0
  30. package/dist/origin-region.js.map +1 -0
  31. package/dist/prefetch-probe-cli.d.ts +36 -0
  32. package/dist/prefetch-probe-cli.d.ts.map +1 -0
  33. package/dist/prefetch-probe-cli.js +163 -0
  34. package/dist/prefetch-probe-cli.js.map +1 -0
  35. package/dist/project.d.ts +20 -0
  36. package/dist/project.d.ts.map +1 -1
  37. package/dist/project.js +92 -2
  38. package/dist/project.js.map +1 -1
  39. package/dist/ready-cli.d.ts.map +1 -1
  40. package/dist/ready-cli.js +8 -3
  41. package/dist/ready-cli.js.map +1 -1
  42. package/dist/rule-registry.d.ts +5 -0
  43. package/dist/rule-registry.d.ts.map +1 -1
  44. package/dist/rule-registry.js +1 -0
  45. package/dist/rule-registry.js.map +1 -1
  46. package/dist/site-system/checks.d.ts +62 -1
  47. package/dist/site-system/checks.d.ts.map +1 -1
  48. package/dist/site-system/checks.js +9 -0
  49. package/dist/site-system/checks.js.map +1 -1
  50. package/dist/site-system/config.d.ts +27 -0
  51. package/dist/site-system/config.d.ts.map +1 -1
  52. package/dist/site-system/config.js +50 -1
  53. package/dist/site-system/config.js.map +1 -1
  54. package/dist/site-system/index.d.ts +1 -1
  55. package/dist/site-system/index.d.ts.map +1 -1
  56. package/dist/site-system/index.js +1 -1
  57. package/dist/site-system/index.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +9 -2
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +11 -1
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
  63. package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
  64. package/dist/site-system/journeys/prefetch-measure.js +94 -0
  65. package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
  66. package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
  67. package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
  68. package/dist/site-system/journeys/prefetch-policy.js +152 -0
  69. package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
  70. package/dist/site-system/journeys/prefetch.d.ts +3 -0
  71. package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
  72. package/dist/site-system/journeys/prefetch.js +37 -0
  73. package/dist/site-system/journeys/prefetch.js.map +1 -0
  74. package/dist/site-system/journeys/variants.d.ts +34 -0
  75. package/dist/site-system/journeys/variants.d.ts.map +1 -0
  76. package/dist/site-system/journeys/variants.js +139 -0
  77. package/dist/site-system/journeys/variants.js.map +1 -0
  78. package/dist/site-system/report.d.ts.map +1 -1
  79. package/dist/site-system/report.js +19 -5
  80. package/dist/site-system/report.js.map +1 -1
  81. package/dist/site-system/rule-modules.d.ts.map +1 -1
  82. package/dist/site-system/rule-modules.js +10 -0
  83. package/dist/site-system/rule-modules.js.map +1 -1
  84. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  85. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  86. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  87. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  88. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  89. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  90. package/dist/site-system/rules/delivery/index.js +7 -0
  91. package/dist/site-system/rules/delivery/index.js.map +1 -0
  92. package/dist/site-system/rules/full/common.d.ts +3 -0
  93. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  94. package/dist/site-system/rules/full/common.js.map +1 -1
  95. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  96. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  97. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  98. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  99. package/dist/site-system/rules/full/estate.d.ts +20 -1
  100. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  101. package/dist/site-system/rules/full/estate.js +33 -5
  102. package/dist/site-system/rules/full/estate.js.map +1 -1
  103. package/dist/site-system/rules/full/geography.d.ts +8 -2
  104. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  105. package/dist/site-system/rules/full/geography.js +6 -2
  106. package/dist/site-system/rules/full/geography.js.map +1 -1
  107. package/dist/site-system/rules/full/index.d.ts +3 -2
  108. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  109. package/dist/site-system/rules/full/index.js +27 -9
  110. package/dist/site-system/rules/full/index.js.map +1 -1
  111. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  112. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  113. package/dist/site-system/rules/full/k12-slots.js +40 -0
  114. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  115. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  116. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  117. package/dist/site-system/rules/full/loc-index.js +43 -2
  118. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  119. package/dist/site-system/rules/full/strategy.d.ts +4 -0
  120. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  121. package/dist/site-system/rules/full/strategy.js +19 -3
  122. package/dist/site-system/rules/full/strategy.js.map +1 -1
  123. package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
  124. package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
  125. package/dist/site-system/rules/full/w3-refresh.js +70 -0
  126. package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
  127. package/dist/site-system/rules/guards/index.d.ts +22 -0
  128. package/dist/site-system/rules/guards/index.d.ts.map +1 -0
  129. package/dist/site-system/rules/guards/index.js +109 -0
  130. package/dist/site-system/rules/guards/index.js.map +1 -0
  131. package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
  132. package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
  133. package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
  134. package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
  135. package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
  136. package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
  137. package/dist/site-system/rules/guards/w9-params.js +84 -0
  138. package/dist/site-system/rules/guards/w9-params.js.map +1 -0
  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 +6 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/runners.js +1 -1
  144. package/dist/site-system/runners.js.map +1 -1
  145. package/dist/site-system-e2e.d.ts +1 -0
  146. package/dist/site-system-e2e.d.ts.map +1 -1
  147. package/dist/site-system-e2e.js +8 -2
  148. package/dist/site-system-e2e.js.map +1 -1
  149. package/package.json +6 -4
package/README.md CHANGED
@@ -21,7 +21,9 @@ Proprietary software © Brandlab Digital Marketing. All rights reserved. Publish
21
21
  A site adopts Site System v2 by declaring `siteSystem: { tier: 'shell' | 'enquiry' | 'full' }` in
22
22
  `brandlab.config.ts` (`newBuild: true` for new builds and redesigns; `samples` adds pages to check; `labels`,
23
23
  `freeQuotes`, `thirdParties` and `phones` record the quote labels, free-quote offer, third-party hosts and extra
24
- `tel:` links the shell rules accept). Without it, nothing in the gate blocks.
24
+ `tel:` links the shell rules accept; `refreshExceptions` lists estate families allowed to refresh more often than
25
+ every 7 days, each `{ family, revalidateSeconds, reason, decisionId }`, for W3 from 3.13.0). Without it, nothing in
26
+ the gate blocks.
25
27
 
26
28
  - `brandlab-ready` (after `next build` and the site-system journeys) writes `docs/ready/ready-report.md` and
27
29
  `.json`: every check with its tier, whether it blocks for this site, its status (pass, fail, not run, excepted,
@@ -41,3 +43,41 @@ A site adopts Site System v2 by declaring `siteSystem: { tier: 'shell' | 'enquir
41
43
  `brandlab-ready --launch` also fails until every step is recorded.
42
44
  - Rule modules export a `SiteSystemRenderedRunner` from `@brandlab-digital/conformance/site-system` and are
43
45
  listed in `SITE_SYSTEM_RENDERED_RUNNERS`.
46
+ - W2 (shell tier, from 3.13.0) runs inside the site-system journeys on each sampled page at 390 x 844 and
47
+ 1440 x 900: at most 8 prefetched pages on landing and 12 after a full scroll; none from footer, header panel,
48
+ drawer, area (`coverage`), nearby (`nearby-areas`), map (`area-map`, `data-area-map`), finder or other-services
49
+ links (a site marks its own lists with `data-bl-links="area|nearby|map|finder|other-services"`), none through a
50
+ hidden link and never the current page. Compressed prefetch bytes above twice the page's own HTML add a warning.
51
+ - `brandlab-prefetch-probe <url> [--viewport 390x844 ...] [--json] [--strict]` measures one page locally (a local
52
+ build, a preview or a live page) in headless Chromium: the pages prefetched on landing and after a full scroll at
53
+ each viewport, their compressed and decoded bytes, the edge cache result and the W2 result. No request leaves for
54
+ another site, and it never crawls.
55
+
56
+ ## Cost guards (from 3.13.0)
57
+
58
+ - **W5** (static, shell): `vercel.json` `regions` equal `brandlab.config.ts` `hosting.originRegion` (Sydney,
59
+ `syd1`, when there is no hosting block). Any other region needs an `origin-region-v1` decision whose `reviewBy`
60
+ is at most six months after `decidedOn`; the check fails once `reviewBy` passes (Sydney time). The engineering
61
+ check `vercel-region` (`brandlab-verify --engineering`, inside every site's `npm run verify`) applies the same
62
+ rule except the date: a lapsed `reviewBy` never stops a build, a fix or a rollback.
63
+ - **W10** (rendered, shell): the built `robots.txt` (or the running site's, with `--base-url`; a Preview that
64
+ answers noindex, or a build made for a Preview whose `robots.txt` disallows everything, is not judged) lets every search and answer crawler (site-core `BRANDLAB_SEARCH_CRAWLERS`) fetch
65
+ home and the representative routes, and gives none of them a Crawl-delay. Firewall rules that match on the user
66
+ agent (`vercel.json` `routes` with `mitigate`, and `hosting.firewall`) must not deny or challenge them; a rate
67
+ limit on one is a warning.
68
+ - **W9** (journey, full): `defineBrandlabSiteSystemTests({ pages: { families: [...] } })` names one built page per
69
+ dynamic route family; an unknown slug answers the shared 404, the uppercase slug answers it too or redirects once
70
+ (301 or 308) to its exact lowercase form, and `?utm_source=x` serves the same cacheable page as the clean URL. **W9-params** (static, full): no page under a dynamic route reads `searchParams`.
71
+ - **K12-slots** (rendered, full, from 3.14.0): the lines a trade requires inside a section (site-core
72
+ `SECTION_TRADE_PROFILES`, for the trade `defineSectionOrder` names): for trees, the council permit note in
73
+ `LocalFacts` on suburb and service-in-suburb pages and the qualifications and insurance lines on the about page. The
74
+ section carries `data-bl-slot="<slot>"`, or lists `<slot>:no-…` in `data-bl-slot-skip` (a warning, so the record is
75
+ re-checked). **E1-pattern** (rendered, full, from 3.14.0): every location page outside `docs/recorded-titles.json`
76
+ has the H1 "{Service} {Place}" (no "in", no "the") and a title that starts with it, at most 60 characters
77
+ (site-core `locationHeadingProblem`). **LOC-INDEX** also reads `estate` in strategy.ts (3.14.0): every location page
78
+ is indexed now or in a wave, a `return` wave's pages are in the build already, and a region page at a suburb's slug
79
+ sends the suburb to "{slug}-suburb"; renamed slugs are read as their registry places.
80
+ - `brandlab-edge-probe [--json] <url>...` (run locally as `npm run probe:edge`, at most 25 URLs) logs
81
+ `x-vercel-cache`, `age`, `x-vercel-id` (edge and function region) and time to first byte for each page, its RSC
82
+ payload and its `_tree` prefetch, and a GET to each origin's `/api/contact` (expected 405). It is the measuring
83
+ stick of the origin-region benchmark (`operations/shared-platform/ORIGIN_REGION.md`).
@@ -1,19 +1,3 @@
1
- /**
2
- * The shared analytics journeys for a Brandlab production build. Google is
3
- * never contacted: the tag loader is stubbed and every dataLayer command is
4
- * recorded in the test process, across navigations.
5
- *
6
- * // tests/e2e/analytics.spec.ts
7
- * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'
8
- *
9
- * defineBrandlabAnalyticsTests({
10
- * paths: ['/', '/services/blocked-drains'],
11
- * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },
12
- * })
13
- *
14
- * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
15
- * so BrandlabGoogleTag renders.
16
- */
17
1
  import { type Page } from '@playwright/test';
18
2
  import { type BrandlabContactJourney } from './contact-e2e.js';
19
3
  export interface BrandlabAnalyticsTestOptions {
@@ -27,7 +11,16 @@ export interface BrandlabAnalyticsTestOptions {
27
11
  };
28
12
  /** Event names the project's analytics contract declares in additionalEvents. */
29
13
  additionalEvents?: readonly string[];
14
+ /**
15
+ * The contract's Enhanced Conversions mode. Defaults to the one `src/config/analytics.ts` declares
16
+ * (`googleAds.enhancedConversions.mode`). Only `disclosed` lets the entered email and phone reach Google Ads, and only
17
+ * through the documented `user_data` path.
18
+ */
19
+ enhancedConversions?: BrandlabEnhancedConversionsCheckMode;
30
20
  }
21
+ export type BrandlabEnhancedConversionsCheckMode = 'disclosed' | 'consent-required' | 'off';
22
+ /** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */
23
+ export declare function readBrandlabEnhancedConversionsMode(root?: string): BrandlabEnhancedConversionsCheckMode;
31
24
  /** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
32
25
  export declare const BRANDLAB_RETIRED_ANALYTICS_EVENTS: readonly string[];
33
26
  /**
@@ -47,6 +40,7 @@ export declare function analyticsEvents(commands: readonly AnalyticsCommand[], n
47
40
  export declare function analyticsEventProblems(commands: readonly AnalyticsCommand[], options?: {
48
41
  enteredValues?: readonly string[];
49
42
  additionalEvents?: readonly string[];
43
+ enhancedConversions?: BrandlabEnhancedConversionsCheckMode;
50
44
  }): string[];
51
45
  export declare function defineBrandlabAnalyticsTests(options?: BrandlabAnalyticsTestOptions): void;
52
46
  //# sourceMappingURL=analytics-e2e.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics-e2e.d.ts","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAA2C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAQxG,MAAM,WAAW,4BAA4B;IAC3C,4EAA4E;IAC5E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,sBAAsB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/D,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,2GAA2G;AAC3G,eAAO,MAAM,iCAAiC,mBAE5C,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mBAEpC,CAAC;AAKH,MAAM,MAAM,gBAAgB,GAAG,OAAO,EAAE,CAAC;AAEzC,kFAAkF;AAClF,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+BrF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;UAGvC,MAAM;YAAgC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzG;AAED,gGAAgG;AAChG,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EACrC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GACxF,MAAM,EAAE,CAiBV;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,4BAAiC,QAiGtF"}
1
+ {"version":3,"file":"analytics-e2e.d.ts","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAA2C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAQxG,MAAM,WAAW,4BAA4B;IAC3C,4EAA4E;IAC5E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,sBAAsB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/D,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;CAC5D;AAED,MAAM,MAAM,oCAAoC,GAAG,WAAW,GAAG,kBAAkB,GAAG,KAAK,CAAC;AAE5F,sHAAsH;AACtH,wBAAgB,mCAAmC,CAAC,IAAI,GAAE,MAAsB,GAAG,oCAAoC,CAStH;AAED,2GAA2G;AAC3G,eAAO,MAAM,iCAAiC,mBAE5C,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mBAEpC,CAAC;AAKH,MAAM,MAAM,gBAAgB,GAAG,OAAO,EAAE,CAAC;AAEzC,kFAAkF;AAClF,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+BrF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;UAGvC,MAAM;YAAgC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzG;AAyCD,gGAAgG;AAChG,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EACrC,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;CACvD,GACL,MAAM,EAAE,CAqBV;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,4BAAiC,QAkGtF"}
@@ -14,9 +14,23 @@
14
14
  * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
15
15
  * so BrandlabGoogleTag renders.
16
16
  */
17
+ import { readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
17
19
  import { expect, test } from '@playwright/test';
18
20
  import { fillBrandlabContactForm, successPattern } from './contact-e2e.js';
19
21
  import { ACCEPTED_CONTACT_RESULT, RETRYABLE_CONTACT_FAILURE, contactEndpointMatcher, isolateFromNetwork, } from './e2e-support.js';
22
+ /** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */
23
+ export function readBrandlabEnhancedConversionsMode(root = process.cwd()) {
24
+ let source = '';
25
+ try {
26
+ source = readFileSync(join(root, 'src/config/analytics.ts'), 'utf8');
27
+ }
28
+ catch {
29
+ return 'off';
30
+ }
31
+ const mode = /enhancedConversions\s*:\s*\{[^}]*\bmode\s*:\s*['"]([a-z-]+)['"]/.exec(source)?.[1];
32
+ return mode === 'disclosed' || mode === 'consent-required' ? mode : 'off';
33
+ }
20
34
  /** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
21
35
  export const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([
22
36
  'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',
@@ -72,6 +86,45 @@ export function analyticsEvents(commands, name) {
72
86
  .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))
73
87
  .map((command) => ({ name: command[1], params: (command[2] ?? {}) }));
74
88
  }
89
+ const EMAIL_VALUE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
90
+ const digitsOf = (value) => value.replace(/\D/g, '');
91
+ /** Whether a user_data field is the visitor's entered email (`email`) or phone (`phone_number`, normalised to E.164). */
92
+ function isEnteredContactField(key, value, entered) {
93
+ if (typeof value !== 'string')
94
+ return false;
95
+ if (key === 'email') {
96
+ return entered.some((item) => EMAIL_VALUE.test(item) && item.toLowerCase() === value.trim().toLowerCase());
97
+ }
98
+ if (key === 'phone_number') {
99
+ const field = digitsOf(value);
100
+ return field.length >= 8 && entered.some((item) => {
101
+ const digits = digitsOf(item);
102
+ return /^[\d\s()+-]+$/.test(item) && digits.length >= 8
103
+ && (field === digits || field.endsWith(digits.replace(/^0/, '')));
104
+ });
105
+ }
106
+ return false;
107
+ }
108
+ function withoutEnteredContact(data, entered) {
109
+ if (!data || typeof data !== 'object' || Array.isArray(data))
110
+ return data;
111
+ return Object.fromEntries(Object.entries(data).filter(([key, value]) => !isEnteredContactField(key, value, entered)));
112
+ }
113
+ /**
114
+ * Disclosed Enhanced Conversions send the entered email and phone to Google Ads by design, through `gtag('set',
115
+ * 'user_data', …)` (the shared runtime) or a conversion's `user_data` parameter. Only those two fields, in only those
116
+ * two places, are taken out before the entered-value check; everything else is still checked.
117
+ */
118
+ function withoutDisclosedUserData(command, entered) {
119
+ if (command[0] === 'set' && command[1] === 'user_data')
120
+ return ['set', 'user_data', withoutEnteredContact(command[2], entered)];
121
+ if (command[0] === 'event' && command[1] === 'conversion' && command[2] && typeof command[2] === 'object') {
122
+ const params = command[2];
123
+ if ('user_data' in params)
124
+ return ['event', 'conversion', { ...params, user_data: withoutEnteredContact(params.user_data, entered) }];
125
+ }
126
+ return command;
127
+ }
75
128
  /** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */
76
129
  export function analyticsEventProblems(commands, options = {}) {
77
130
  const problems = [];
@@ -90,8 +143,12 @@ export function analyticsEventProblems(commands, options = {}) {
90
143
  problems.push(`${event.name} carries the ${key} parameter`);
91
144
  }
92
145
  }
93
- const serialised = JSON.stringify(commands).toLowerCase();
94
- for (const value of (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5)) {
146
+ const entered = (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5);
147
+ const checked = options.enhancedConversions === 'disclosed'
148
+ ? commands.map((command) => withoutDisclosedUserData(command, entered))
149
+ : commands;
150
+ const serialised = JSON.stringify(checked).toLowerCase();
151
+ for (const value of entered) {
95
152
  if (serialised.includes(value.toLowerCase()))
96
153
  problems.push(`an analytics command contains the entered value "${value}"`);
97
154
  }
@@ -100,6 +157,7 @@ export function analyticsEventProblems(commands, options = {}) {
100
157
  export function defineBrandlabAnalyticsTests(options = {}) {
101
158
  const paths = options.paths?.length ? options.paths : ['/'];
102
159
  const additionalEvents = options.additionalEvents ?? [];
160
+ const enhancedConversions = options.enhancedConversions ?? readBrandlabEnhancedConversionsMode();
103
161
  test.describe('Brandlab analytics', () => {
104
162
  // Service workers could answer requests outside page and context routing.
105
163
  test.use({ serviceWorkers: 'block' });
@@ -168,7 +226,7 @@ export function defineBrandlabAnalyticsTests(options = {}) {
168
226
  await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);
169
227
  await page.waitForTimeout(500);
170
228
  expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);
171
- expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);
229
+ expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents, enhancedConversions })).toEqual([]);
172
230
  });
173
231
  }
174
232
  test('sends no retired or web_vital events', async ({ page }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"analytics-e2e.js","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared analytics journeys for a Brandlab production build. Google is\n * never contacted: the tag loader is stubbed and every dataLayer command is\n * recorded in the test process, across navigations.\n *\n * // tests/e2e/analytics.spec.ts\n * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'\n *\n * defineBrandlabAnalyticsTests({\n * paths: ['/', '/services/blocked-drains'],\n * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },\n * })\n *\n * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)\n * so BrandlabGoogleTag renders.\n */\nimport { expect, test, type Page } from '@playwright/test';\nimport { fillBrandlabContactForm, successPattern, type BrandlabContactJourney } from './contact-e2e.js';\nimport {\n ACCEPTED_CONTACT_RESULT,\n RETRYABLE_CONTACT_FAILURE,\n contactEndpointMatcher,\n isolateFromNetwork,\n} from './e2e-support.js';\n\nexport interface BrandlabAnalyticsTestOptions {\n /** Pages to load; the first renders a telephone link. Defaults to ['/']. */\n paths?: readonly string[];\n /** Selector of the telephone link to tap; defaults to the first visible tel: link. */\n phoneLink?: string;\n /** The contact journey whose accepted result emits one contact_form. */\n contact?: BrandlabContactJourney & { retry?: string | RegExp };\n /** Event names the project's analytics contract declares in additionalEvents. */\n additionalEvents?: readonly string[];\n}\n\n/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */\nexport const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([\n 'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',\n]);\n\n/**\n * The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,\n * CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The\n * Google Ads `conversion` event is also allowed, only with an AW- destination.\n */\nexport const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([\n 'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',\n]);\n\nconst PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;\nconst PHONE_DEDUPE_WINDOW_MS = 1_200;\n\nexport type AnalyticsCommand = unknown[];\n\n/** Record every gtag/dataLayer command the page pushes, surviving navigations. */\nexport async function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]> {\n const commands: AnalyticsCommand[] = [];\n await page.exposeFunction('__brandlabRecordAnalytics', (command: AnalyticsCommand) => { commands.push(command); });\n await page.addInitScript(() => {\n const record = (entry: unknown) => {\n try {\n const command = entry && typeof entry === 'object' && 'length' in (entry as object)\n ? Array.from(entry as ArrayLike<unknown>)\n : [entry];\n void (window as unknown as { __brandlabRecordAnalytics: (value: unknown) => Promise<void> })\n .__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));\n } catch { /* an unserialisable command is not analytics evidence */ }\n };\n const wrapped = new WeakSet<unknown[]>();\n const wrap = (layer: unknown[]) => {\n if (wrapped.has(layer)) return layer;\n wrapped.add(layer);\n const push = layer.push.bind(layer);\n layer.push = (...entries: unknown[]) => { entries.forEach(record); return push(...entries); };\n layer.forEach(record);\n return layer;\n };\n let dataLayer = wrap([]);\n Object.defineProperty(window, 'dataLayer', {\n configurable: true,\n get: () => dataLayer,\n // `window.dataLayer = window.dataLayer || []` reassigns the same array.\n set: (value: unknown[]) => { if (Array.isArray(value)) dataLayer = wrap(value); },\n });\n });\n return commands;\n}\n\nexport function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string) {\n return commands\n .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))\n .map((command) => ({ name: command[1] as string, params: (command[2] ?? {}) as Record<string, unknown> }));\n}\n\n/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */\nexport function analyticsEventProblems(\n commands: readonly AnalyticsCommand[],\n options: { enteredValues?: readonly string[]; additionalEvents?: readonly string[] } = {},\n): string[] {\n const problems: string[] = [];\n const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);\n for (const event of analyticsEvents(commands)) {\n if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name)) problems.push(`retired event ${event.name}`);\n else if (event.name === 'conversion') {\n if (!String(event.params.send_to ?? '').startsWith('AW-')) problems.push('conversion event without a Google Ads destination');\n } else if (!allowed.has(event.name)) problems.push(`unexpected event ${event.name}`);\n for (const key of Object.keys(event.params)) {\n if (PII_PARAMETER.test(key)) problems.push(`${event.name} carries the ${key} parameter`);\n }\n }\n const serialised = JSON.stringify(commands).toLowerCase();\n for (const value of (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5)) {\n if (serialised.includes(value.toLowerCase())) problems.push(`an analytics command contains the entered value \"${value}\"`);\n }\n return problems;\n}\n\nexport function defineBrandlabAnalyticsTests(options: BrandlabAnalyticsTestOptions = {}) {\n const paths = options.paths?.length ? options.paths : ['/'];\n const additionalEvents = options.additionalEvents ?? [];\n\n test.describe('Brandlab analytics', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n let commands: AnalyticsCommand[] = [];\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n commands = await recordAnalyticsCommands(page);\n });\n\n test('renders exactly one Google tag on every page', async ({ page }) => {\n for (const path of paths) {\n commands.length = 0;\n // DOMContentLoaded, not load: next start's image optimiser can stall concurrent first image requests\n // (vercel/next.js#98168), and nothing here needs the images. The expectations below wait for the tag.\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n const loaders = page.locator('script[src*=\"googletagmanager.com/gtag/js\"]');\n await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)\n .toBeAttached({ timeout: 15_000 });\n expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);\n const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));\n expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);\n }\n });\n\n test('sends one phone_link_click per tap', async ({ page }) => {\n await page.goto(paths[0]!, { waitUntil: 'domcontentloaded' });\n const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^=\"tel:\"]:visible').first();\n await expect(link, 'the page renders a telephone link').toBeVisible();\n await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);\n // Keep the page in place; the shared listener still sees the tap.\n await page.evaluate(() => window.addEventListener('click', (event) => {\n if ((event.target as Element | null)?.closest?.('a[href^=\"tel:\"]')) event.preventDefault();\n }, true));\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);\n await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);\n expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);\n for (const event of analyticsEvents(commands, 'phone_link_click')) {\n expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n\n if (options.contact) {\n const journey = options.contact;\n test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {\n let attempts = 0;\n await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {\n attempts += 1;\n await route.fulfill(attempts === 1\n ? { status: 503, json: RETRYABLE_CONTACT_FAILURE }\n : { status: 200, json: ACCEPTED_CONTACT_RESULT });\n });\n await fillBrandlabContactForm(page, journey);\n const submit = page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });\n await submit.click();\n await expect.poll(() => attempts).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);\n const retry = journey.retry ?? journey.submit;\n await page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();\n await expect(page).toHaveURL(successPattern(journey.successPath));\n await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);\n expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);\n });\n }\n\n test('sends no retired or web_vital events', async ({ page }) => {\n for (const path of paths) {\n const tagRuns = commands.filter((command) => command[0] === 'js').length;\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n // The page's instrumentation has run once its Google tag has: any reporter would be listening by then.\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: the Google tag runs`).toBeGreaterThan(tagRuns);\n await page.mouse.wheel(0, 1_200);\n await page.waitForTimeout(300);\n // Web-vitals reporters flush when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });\n document.dispatchEvent(new Event('visibilitychange'));\n window.dispatchEvent(new Event('pagehide'));\n });\n await page.waitForTimeout(300);\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n });\n}\n"]}
1
+ {"version":3,"file":"analytics-e2e.js","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared analytics journeys for a Brandlab production build. Google is\n * never contacted: the tag loader is stubbed and every dataLayer command is\n * recorded in the test process, across navigations.\n *\n * // tests/e2e/analytics.spec.ts\n * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'\n *\n * defineBrandlabAnalyticsTests({\n * paths: ['/', '/services/blocked-drains'],\n * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },\n * })\n *\n * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)\n * so BrandlabGoogleTag renders.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { expect, test, type Page } from '@playwright/test';\nimport { fillBrandlabContactForm, successPattern, type BrandlabContactJourney } from './contact-e2e.js';\nimport {\n ACCEPTED_CONTACT_RESULT,\n RETRYABLE_CONTACT_FAILURE,\n contactEndpointMatcher,\n isolateFromNetwork,\n} from './e2e-support.js';\n\nexport interface BrandlabAnalyticsTestOptions {\n /** Pages to load; the first renders a telephone link. Defaults to ['/']. */\n paths?: readonly string[];\n /** Selector of the telephone link to tap; defaults to the first visible tel: link. */\n phoneLink?: string;\n /** The contact journey whose accepted result emits one contact_form. */\n contact?: BrandlabContactJourney & { retry?: string | RegExp };\n /** Event names the project's analytics contract declares in additionalEvents. */\n additionalEvents?: readonly string[];\n /**\n * The contract's Enhanced Conversions mode. Defaults to the one `src/config/analytics.ts` declares\n * (`googleAds.enhancedConversions.mode`). Only `disclosed` lets the entered email and phone reach Google Ads, and only\n * through the documented `user_data` path.\n */\n enhancedConversions?: BrandlabEnhancedConversionsCheckMode;\n}\n\nexport type BrandlabEnhancedConversionsCheckMode = 'disclosed' | 'consent-required' | 'off';\n\n/** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */\nexport function readBrandlabEnhancedConversionsMode(root: string = process.cwd()): BrandlabEnhancedConversionsCheckMode {\n let source = '';\n try {\n source = readFileSync(join(root, 'src/config/analytics.ts'), 'utf8');\n } catch {\n return 'off';\n }\n const mode = /enhancedConversions\\s*:\\s*\\{[^}]*\\bmode\\s*:\\s*['\"]([a-z-]+)['\"]/.exec(source)?.[1];\n return mode === 'disclosed' || mode === 'consent-required' ? mode : 'off';\n}\n\n/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */\nexport const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([\n 'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',\n]);\n\n/**\n * The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,\n * CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The\n * Google Ads `conversion` event is also allowed, only with an AW- destination.\n */\nexport const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([\n 'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',\n]);\n\nconst PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;\nconst PHONE_DEDUPE_WINDOW_MS = 1_200;\n\nexport type AnalyticsCommand = unknown[];\n\n/** Record every gtag/dataLayer command the page pushes, surviving navigations. */\nexport async function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]> {\n const commands: AnalyticsCommand[] = [];\n await page.exposeFunction('__brandlabRecordAnalytics', (command: AnalyticsCommand) => { commands.push(command); });\n await page.addInitScript(() => {\n const record = (entry: unknown) => {\n try {\n const command = entry && typeof entry === 'object' && 'length' in (entry as object)\n ? Array.from(entry as ArrayLike<unknown>)\n : [entry];\n void (window as unknown as { __brandlabRecordAnalytics: (value: unknown) => Promise<void> })\n .__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));\n } catch { /* an unserialisable command is not analytics evidence */ }\n };\n const wrapped = new WeakSet<unknown[]>();\n const wrap = (layer: unknown[]) => {\n if (wrapped.has(layer)) return layer;\n wrapped.add(layer);\n const push = layer.push.bind(layer);\n layer.push = (...entries: unknown[]) => { entries.forEach(record); return push(...entries); };\n layer.forEach(record);\n return layer;\n };\n let dataLayer = wrap([]);\n Object.defineProperty(window, 'dataLayer', {\n configurable: true,\n get: () => dataLayer,\n // `window.dataLayer = window.dataLayer || []` reassigns the same array.\n set: (value: unknown[]) => { if (Array.isArray(value)) dataLayer = wrap(value); },\n });\n });\n return commands;\n}\n\nexport function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string) {\n return commands\n .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))\n .map((command) => ({ name: command[1] as string, params: (command[2] ?? {}) as Record<string, unknown> }));\n}\n\nconst EMAIL_VALUE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst digitsOf = (value: string) => value.replace(/\\D/g, '');\n\n/** Whether a user_data field is the visitor's entered email (`email`) or phone (`phone_number`, normalised to E.164). */\nfunction isEnteredContactField(key: string, value: unknown, entered: readonly string[]): boolean {\n if (typeof value !== 'string') return false;\n if (key === 'email') {\n return entered.some((item) => EMAIL_VALUE.test(item) && item.toLowerCase() === value.trim().toLowerCase());\n }\n if (key === 'phone_number') {\n const field = digitsOf(value);\n return field.length >= 8 && entered.some((item) => {\n const digits = digitsOf(item);\n return /^[\\d\\s()+-]+$/.test(item) && digits.length >= 8\n && (field === digits || field.endsWith(digits.replace(/^0/, '')));\n });\n }\n return false;\n}\n\nfunction withoutEnteredContact(data: unknown, entered: readonly string[]): unknown {\n if (!data || typeof data !== 'object' || Array.isArray(data)) return data;\n return Object.fromEntries(Object.entries(data).filter(([key, value]) => !isEnteredContactField(key, value, entered)));\n}\n\n/**\n * Disclosed Enhanced Conversions send the entered email and phone to Google Ads by design, through `gtag('set',\n * 'user_data', …)` (the shared runtime) or a conversion's `user_data` parameter. Only those two fields, in only those\n * two places, are taken out before the entered-value check; everything else is still checked.\n */\nfunction withoutDisclosedUserData(command: AnalyticsCommand, entered: readonly string[]): AnalyticsCommand {\n if (command[0] === 'set' && command[1] === 'user_data') return ['set', 'user_data', withoutEnteredContact(command[2], entered)];\n if (command[0] === 'event' && command[1] === 'conversion' && command[2] && typeof command[2] === 'object') {\n const params = command[2] as Record<string, unknown>;\n if ('user_data' in params) return ['event', 'conversion', { ...params, user_data: withoutEnteredContact(params.user_data, entered) }];\n }\n return command;\n}\n\n/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */\nexport function analyticsEventProblems(\n commands: readonly AnalyticsCommand[],\n options: {\n enteredValues?: readonly string[];\n additionalEvents?: readonly string[];\n enhancedConversions?: BrandlabEnhancedConversionsCheckMode;\n } = {},\n): string[] {\n const problems: string[] = [];\n const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);\n for (const event of analyticsEvents(commands)) {\n if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name)) problems.push(`retired event ${event.name}`);\n else if (event.name === 'conversion') {\n if (!String(event.params.send_to ?? '').startsWith('AW-')) problems.push('conversion event without a Google Ads destination');\n } else if (!allowed.has(event.name)) problems.push(`unexpected event ${event.name}`);\n for (const key of Object.keys(event.params)) {\n if (PII_PARAMETER.test(key)) problems.push(`${event.name} carries the ${key} parameter`);\n }\n }\n const entered = (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5);\n const checked = options.enhancedConversions === 'disclosed'\n ? commands.map((command) => withoutDisclosedUserData(command, entered))\n : commands;\n const serialised = JSON.stringify(checked).toLowerCase();\n for (const value of entered) {\n if (serialised.includes(value.toLowerCase())) problems.push(`an analytics command contains the entered value \"${value}\"`);\n }\n return problems;\n}\n\nexport function defineBrandlabAnalyticsTests(options: BrandlabAnalyticsTestOptions = {}) {\n const paths = options.paths?.length ? options.paths : ['/'];\n const additionalEvents = options.additionalEvents ?? [];\n const enhancedConversions = options.enhancedConversions ?? readBrandlabEnhancedConversionsMode();\n\n test.describe('Brandlab analytics', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n let commands: AnalyticsCommand[] = [];\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n commands = await recordAnalyticsCommands(page);\n });\n\n test('renders exactly one Google tag on every page', async ({ page }) => {\n for (const path of paths) {\n commands.length = 0;\n // DOMContentLoaded, not load: next start's image optimiser can stall concurrent first image requests\n // (vercel/next.js#98168), and nothing here needs the images. The expectations below wait for the tag.\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n const loaders = page.locator('script[src*=\"googletagmanager.com/gtag/js\"]');\n await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)\n .toBeAttached({ timeout: 15_000 });\n expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);\n const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));\n expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);\n }\n });\n\n test('sends one phone_link_click per tap', async ({ page }) => {\n await page.goto(paths[0]!, { waitUntil: 'domcontentloaded' });\n const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^=\"tel:\"]:visible').first();\n await expect(link, 'the page renders a telephone link').toBeVisible();\n await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);\n // Keep the page in place; the shared listener still sees the tap.\n await page.evaluate(() => window.addEventListener('click', (event) => {\n if ((event.target as Element | null)?.closest?.('a[href^=\"tel:\"]')) event.preventDefault();\n }, true));\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);\n await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);\n expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);\n for (const event of analyticsEvents(commands, 'phone_link_click')) {\n expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n\n if (options.contact) {\n const journey = options.contact;\n test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {\n let attempts = 0;\n await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {\n attempts += 1;\n await route.fulfill(attempts === 1\n ? { status: 503, json: RETRYABLE_CONTACT_FAILURE }\n : { status: 200, json: ACCEPTED_CONTACT_RESULT });\n });\n await fillBrandlabContactForm(page, journey);\n const submit = page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });\n await submit.click();\n await expect.poll(() => attempts).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);\n const retry = journey.retry ?? journey.submit;\n await page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();\n await expect(page).toHaveURL(successPattern(journey.successPath));\n await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);\n expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents, enhancedConversions })).toEqual([]);\n });\n }\n\n test('sends no retired or web_vital events', async ({ page }) => {\n for (const path of paths) {\n const tagRuns = commands.filter((command) => command[0] === 'js').length;\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n // The page's instrumentation has run once its Google tag has: any reporter would be listening by then.\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: the Google tag runs`).toBeGreaterThan(tagRuns);\n await page.mouse.wheel(0, 1_200);\n await page.waitForTimeout(300);\n // Web-vitals reporters flush when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });\n document.dispatchEvent(new Event('visibilitychange'));\n window.dispatchEvent(new Event('pagehide'));\n });\n await page.waitForTimeout(300);\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n });\n}\n"]}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,OAAO,EAGL,KAAK,uBAAuB,EAE7B,MAAM,cAAc,CAAC;AAEtB,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,KAAK;IACb,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA0BD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA+BhF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAGH,GACA,MAAM,CAwBR;AA+ED,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAOT"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,OAAO,EAGL,KAAK,uBAAuB,EAE7B,MAAM,cAAc,CAAC;AAEtB,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,KAAK;IACb,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA0BD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA+BhF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAGH,GACA,MAAM,CAwBR;AAoFD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAOT"}
package/dist/cli.js CHANGED
@@ -133,7 +133,10 @@ function formatResult(result) {
133
133
  : "pass";
134
134
  const lines = [
135
135
  "Brandlab verification",
136
- `installed: ${result.installedRelease ?? "unadopted"}`,
136
+ `installed: ${result.installedRelease ??
137
+ (result.canaryRelease
138
+ ? `canary ${result.canaryRelease.version} of ${result.canaryRelease.line} (not a published release)`
139
+ : "unadopted")}`,
137
140
  ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),
138
141
  `policy: ${result.verification.policyVersion}`,
139
142
  `static checks: ${staticStatus}`,
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,yBAAyB,EAAyB,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAEL,+BAA+B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,uBAAuB,EACvB,6BAA6B,GAG9B,MAAM,cAAc,CAAC;AAkBtB,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBZ,CAAC;AAEF,MAAM,UAAU,4BAA4B,CAAC,IAAuB;IAClE,MAAM,MAAM,GAAe;QACzB,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE;QACnB,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI;YAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;aAC9D,IAAI,QAAQ,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;aAC9C,IAAI,QAAQ,KAAK,eAAe;YAAE,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;aAC5D,IAAI,QAAQ,KAAK,kBAAkB;YAAE,MAAM,CAAC,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC;aAC5H,IAAI,QAAQ,KAAK,oBAAoB;YAAE,MAAM,CAAC,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAC,CAAC;aAC3H,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC/D,CAAC;aAAM,IAAI,QAAQ,KAAK,qBAAqB,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,qBAAqB,CAAC,CAAC;YACjE,MAAM,CAAC,gBAAgB,GAAG,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;QACjE,CAAC;aAAM,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YACpD,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,GAAG,CAAC,CAAC;YAC9C,CAAC;YACD,MAAM,CAAC,IAAI,GAAG,KAAgC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,GAAG,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,IAAuB,EACvB,KAAY;IACV,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;IAC9C,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;CAC/C;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,4BAA4B,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAChB,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,CAAC;YACrC,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;YAC1C,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,oBAAoB,EAAE,OAAO,CAAC,oBAAoB;YAClD,GAAG,CAAC,OAAO,CAAC,eAAe;gBACzB,CAAC,CAAC,EAAE,eAAe,EAAE,mBAAmB,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE;gBACnE,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9F,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI;YAC7G,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAChF,OAAO,6BAA6B,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QAClF,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,WAA6B;IACtD,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACpD,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC5E,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QAClF,OAAO,IAAI,KAAK,KAAK,QAAQ,KAAK,OAAO,CAAC,OAAO,IAAI,CAAC;IACxD,CAAC,CAAC,CAAC;IACH,OAAO,0BAA0B,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,QAAQ,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;AACvG,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAY;IACvC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,qBAAqB,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,mBAAmB,CAAC,CAAC;IACnE,CAAC;IACD,+BAA+B,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,MAAiC;IACrD,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CACtD,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CACtC,CAAC;IACF,MAAM,YAAY,GAChB,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC;QACtC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC;YACxC,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM,CAAC;IACf,MAAM,KAAK,GAAG;QACZ,uBAAuB;QACvB,cAAc,MAAM,CAAC,gBAAgB,IAAI,WAAW,EAAE;QACtD,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,kGAAkG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvI,WAAW,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE;QAC9C,kBAAkB,YAAY,EAAE;QAChC,sBACE,cAAc,CAAC,MAAM,GAAG,CAAC;YACvB,CAAC,CAAC,YAAY,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YACtE,CAAC,CAAC,UACN,EAAE;QACF,aAAa,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE;KAC1D,CAAC;IACF,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC;QAClD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM;YAAE,SAAS;QACvC,KAAK,CAAC,IAAI,CACR,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,QAAQ,gBAAgB,MAAM,CAAC,QAAQ,aAAa,CACnG,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI;YAC3B,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;YAC/D,CAAC,CAAC,EAAE,CAAC;QACP,KAAK,CAAC,IAAI,CACR,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,MAAM,GAAG,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,CAC5F,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,SAAS,aAAa,CAAC,OAA6C;IAClE,OAAO,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,QAAQ,gBAAgB,OAAO,CAAC,QAAQ,gBAAgB,OAAO,CAAC,IAAI,OAAO,CAAC;AACnH,CAAC;AAED,SAAS,YAAY,CAAC,IAAuB,EAAE,KAAa,EAAE,QAAgB;IAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,oBAAoB,CAAC,CAAC;IACvF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,SAAiB,EACjB,QAA4B;IAE5B,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,KAAK,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,IAAI,kCAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,OAAO,CAAC,QAAQ,GAAG,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure, type EngineeringCheck } from \"./engineering.js\";\nimport {\n type BrandlabReleaseManifest,\n validateBrandlabReleaseManifest,\n} from \"./release-manifest.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n releaseManifest?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--release-manifest <standards/releases/<version>/manifest.json>]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied. A static check passes only when one of its\nrules ran; otherwise it is reported as not run.\n--release-manifest binds every Brandlab package's lockfile integrity to the\nimmutable release record.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--release-manifest\") parsed.releaseManifest = resolve(requireValue(argv, ++index, \"--release-manifest\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n ...(options.releaseManifest\n ? { releaseManifest: readReleaseManifest(options.releaseManifest) }\n : {}),\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? formatEngineering(engineering) : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatEngineering(engineering: EngineeringCheck): string {\n const findings = engineering.findings.map((finding) => {\n const location = `${finding.path}${finding.line ? `:${finding.line}` : \"\"}`;\n const label = finding.severity === \"info\" ? `${finding.rule}/info` : finding.rule;\n return `[${label}] ${location}: ${finding.message}\\n`;\n });\n return `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${findings.join(\"\")}`;\n}\n\nfunction readReleaseManifest(path: string): BrandlabReleaseManifest {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n throw new Error(`The release manifest ${path} could not be read.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Parser messages quote file content, so only the path is reported.\n throw new Error(`The release manifest ${path} is invalid JSON.`);\n }\n validateBrandlabReleaseManifest(parsed);\n return parsed;\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${result.installedRelease ?? \"unadopted\"}`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure, type EngineeringCheck } from \"./engineering.js\";\nimport {\n type BrandlabReleaseManifest,\n validateBrandlabReleaseManifest,\n} from \"./release-manifest.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n releaseManifest?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--release-manifest <standards/releases/<version>/manifest.json>]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied. A static check passes only when one of its\nrules ran; otherwise it is reported as not run.\n--release-manifest binds every Brandlab package's lockfile integrity to the\nimmutable release record.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--release-manifest\") parsed.releaseManifest = resolve(requireValue(argv, ++index, \"--release-manifest\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n ...(options.releaseManifest\n ? { releaseManifest: readReleaseManifest(options.releaseManifest) }\n : {}),\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? formatEngineering(engineering) : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatEngineering(engineering: EngineeringCheck): string {\n const findings = engineering.findings.map((finding) => {\n const location = `${finding.path}${finding.line ? `:${finding.line}` : \"\"}`;\n const label = finding.severity === \"info\" ? `${finding.rule}/info` : finding.rule;\n return `[${label}] ${location}: ${finding.message}\\n`;\n });\n return `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${findings.join(\"\")}`;\n}\n\nfunction readReleaseManifest(path: string): BrandlabReleaseManifest {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n throw new Error(`The release manifest ${path} could not be read.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Parser messages quote file content, so only the path is reported.\n throw new Error(`The release manifest ${path} is invalid JSON.`);\n }\n validateBrandlabReleaseManifest(parsed);\n return parsed;\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${\n result.installedRelease ??\n (result.canaryRelease\n ? `canary ${result.canaryRelease.version} of ${result.canaryRelease.line} (not a published release)`\n : \"unadopted\")\n }`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
@@ -23,19 +23,19 @@ export const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze({
23
23
  "kind": "brandlab-runtime-release-contract",
24
24
  "packages": {
25
25
  "@brandlab-digital/conformance": {
26
- "version": "3.12.1"
26
+ "version": "3.14.0"
27
27
  },
28
28
  "@brandlab-digital/eslint-plugin": {
29
- "version": "3.12.1"
29
+ "version": "3.14.0"
30
30
  },
31
31
  "@brandlab-digital/site-core": {
32
- "version": "3.12.1"
32
+ "version": "3.14.0"
33
33
  },
34
34
  "@brandlab-digital/site-next": {
35
- "version": "3.12.1"
35
+ "version": "3.14.0"
36
36
  }
37
37
  },
38
- "releaseVersion": "3.12.1",
38
+ "releaseVersion": "3.14.0",
39
39
  "runtime": {
40
40
  "bundler": "turbopack",
41
41
  "cacheComponents": false,
@@ -1 +1 @@
1
- {"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,QAAQ;SACpB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;KACF;IACD,gBAAgB,EAAE,QAAQ;IAC1B,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.12.1\"\n },\n \"@brandlab-digital/eslint-plugin\": {\n \"version\": \"3.12.1\"\n },\n \"@brandlab-digital/site-core\": {\n \"version\": \"3.12.1\"\n },\n \"@brandlab-digital/site-next\": {\n \"version\": \"3.12.1\"\n }\n },\n \"releaseVersion\": \"3.12.1\",\n \"runtime\": {\n \"bundler\": \"turbopack\",\n \"cacheComponents\": false,\n \"next\": \"16.4.2\",\n \"nextVersionPolicy\": \"exact\",\n \"node\": \"24.x\",\n \"packageManager\": \"npm@11.14.1\",\n \"react\": \"19.2.8\",\n \"reactDom\": \"19.2.8\",\n \"router\": \"app\",\n \"sharp\": \"0.35.5\",\n \"typescript\": \"6.0.3\"\n },\n \"schemaVersion\": \"brandlab.runtime-release-contract.v1\"\n},\n) as unknown as BrandlabRuntimeReleaseContract;\n\nvalidateBrandlabRuntimeReleaseContract(BRANDLAB_CURRENT_RUNTIME_RELEASE);\n"]}
1
+ {"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,QAAQ;SACpB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;KACF;IACD,gBAAgB,EAAE,QAAQ;IAC1B,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.14.0\"\n },\n \"@brandlab-digital/eslint-plugin\": {\n \"version\": \"3.14.0\"\n },\n \"@brandlab-digital/site-core\": {\n \"version\": \"3.14.0\"\n },\n \"@brandlab-digital/site-next\": {\n \"version\": \"3.14.0\"\n }\n },\n \"releaseVersion\": \"3.14.0\",\n \"runtime\": {\n \"bundler\": \"turbopack\",\n \"cacheComponents\": false,\n \"next\": \"16.4.2\",\n \"nextVersionPolicy\": \"exact\",\n \"node\": \"24.x\",\n \"packageManager\": \"npm@11.14.1\",\n \"react\": \"19.2.8\",\n \"reactDom\": \"19.2.8\",\n \"router\": \"app\",\n \"sharp\": \"0.35.5\",\n \"typescript\": \"6.0.3\"\n },\n \"schemaVersion\": \"brandlab.runtime-release-contract.v1\"\n},\n) as unknown as BrandlabRuntimeReleaseContract;\n\nvalidateBrandlabRuntimeReleaseContract(BRANDLAB_CURRENT_RUNTIME_RELEASE);\n"]}
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ export declare const EDGE_PROBE_MAX_URLS = 25;
3
+ export type EdgeProbeKind = "page" | "rsc" | "tree" | "contact";
4
+ export interface EdgeProbeRequest {
5
+ readonly kind: EdgeProbeKind;
6
+ readonly url: string;
7
+ readonly headers: Readonly<Record<string, string>>;
8
+ /** The status the request should answer; only the contact GET has one (405). */
9
+ readonly expectStatus?: number;
10
+ }
11
+ export interface EdgeProbeResult {
12
+ readonly kind: EdgeProbeKind;
13
+ readonly url: string;
14
+ readonly status: number | null;
15
+ readonly cache: string | null;
16
+ readonly age: string | null;
17
+ readonly vercelId: string | null;
18
+ /** The function region from x-vercel-id when the request reached a function. */
19
+ readonly functionRegion: string | null;
20
+ readonly edgeRegion: string | null;
21
+ readonly ttfbMs: number | null;
22
+ readonly problem?: string;
23
+ }
24
+ /** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */
25
+ export declare function planEdgeProbe(urls: readonly string[]): EdgeProbeRequest[];
26
+ /** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */
27
+ export declare function vercelIdRegions(vercelId: string | null): {
28
+ edge: string | null;
29
+ function: string | null;
30
+ };
31
+ /** One result as a row of the plain-text table. */
32
+ export declare function formatEdgeProbeRow(result: EdgeProbeResult): string;
33
+ type Fetcher = (url: string, init: {
34
+ headers: Record<string, string>;
35
+ redirect: "manual";
36
+ signal?: AbortSignal;
37
+ }) => Promise<{
38
+ status: number;
39
+ headers: {
40
+ get(name: string): string | null;
41
+ };
42
+ body?: {
43
+ cancel(): Promise<void>;
44
+ } | null;
45
+ }>;
46
+ export declare function runEdgeProbe(requests: readonly EdgeProbeRequest[], options?: {
47
+ fetch?: Fetcher;
48
+ now?: () => number;
49
+ }): Promise<EdgeProbeResult[]>;
50
+ export declare function runEdgeProbeCli(argv: readonly string[], io: {
51
+ stdout: (text: string) => void;
52
+ stderr: (text: string) => void;
53
+ }, options?: {
54
+ fetch?: Fetcher;
55
+ }): Promise<number>;
56
+ export {};
57
+ //# sourceMappingURL=edge-probe-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-probe-cli.d.ts","sourceRoot":"","sources":["../src/edge-probe-cli.ts"],"names":[],"mappings":";AAgBA,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAsBzE;AAED,uHAAuH;AACvH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAKzG;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAWlE;AAED,KAAK,OAAO,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC;IAC3H,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAC3C,CAAC,CAAC;AAEH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CA8B3J;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAczL"}