@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
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * brandlab-edge-probe (3.13.0): `npm run probe:edge`, run on a laptop, never in CI. For each fixed URL it requests the
4
+ * page, its RSC payload and its `_tree` segment prefetch the way the Next 16 router does, then makes one GET to
5
+ * `/api/contact` on each origin (which answers 405), and logs `x-vercel-cache`, `age`, `x-vercel-id` and the time to
6
+ * first byte. It is the measuring stick for the origin-region benchmark (operations/shared-platform/ORIGIN_REGION.md):
7
+ * `x-vercel-id` reads `<edge>::<function region>::<id>`, so `syd1::iad1::…` on `/api/contact` confirms a Washington
8
+ * function. A handful of requests, never a crawl: at most 25 URLs.
9
+ *
10
+ * npx brandlab-edge-probe https://www.example.com.au/ https://www.example.com.au/services/blocked-drains
11
+ * npx brandlab-edge-probe --json https://www.example.com.au/
12
+ */
13
+ import { realpathSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ export const EDGE_PROBE_MAX_URLS = 25;
17
+ /** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */
18
+ export function planEdgeProbe(urls) {
19
+ if (urls.length === 0)
20
+ throw new Error("Name at least one URL to probe.");
21
+ if (urls.length > EDGE_PROBE_MAX_URLS)
22
+ throw new Error(`Probe at most ${EDGE_PROBE_MAX_URLS} URLs; this is a measurement, not a crawl.`);
23
+ const requests = [];
24
+ const origins = [];
25
+ for (const raw of urls) {
26
+ let url;
27
+ try {
28
+ url = new URL(raw);
29
+ }
30
+ catch {
31
+ throw new Error(`"${raw}" is not an absolute URL such as https://www.example.com.au/services.`);
32
+ }
33
+ if (url.protocol !== "https:" && url.protocol !== "http:")
34
+ throw new Error(`"${raw}" is not an http(s) URL.`);
35
+ url.hash = "";
36
+ const href = url.toString();
37
+ requests.push({ kind: "page", url: href, headers: { accept: "text/html" } });
38
+ requests.push({ kind: "rsc", url: href, headers: { rsc: "1" } });
39
+ requests.push({ kind: "tree", url: href, headers: { rsc: "1", "next-router-prefetch": "1", "next-router-segment-prefetch": "/_tree" } });
40
+ if (!origins.includes(url.origin))
41
+ origins.push(url.origin);
42
+ }
43
+ for (const origin of origins)
44
+ requests.push({ kind: "contact", url: `${origin}/api/contact`, headers: { accept: "application/json" }, expectStatus: 405 });
45
+ return requests;
46
+ }
47
+ /** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */
48
+ export function vercelIdRegions(vercelId) {
49
+ if (!vercelId)
50
+ return { edge: null, function: null };
51
+ const parts = vercelId.split("::");
52
+ const region = (part) => (part && /^[a-z]{3}\d$/.test(part) ? part : null);
53
+ return { edge: region(parts[0]), function: parts.length >= 3 ? region(parts[1]) : null };
54
+ }
55
+ /** One result as a row of the plain-text table. */
56
+ export function formatEdgeProbeRow(result) {
57
+ const cells = [
58
+ result.kind.padEnd(7),
59
+ String(result.status ?? "-").padEnd(4),
60
+ (result.cache ?? "-").padEnd(8),
61
+ `age ${result.age ?? "-"}`.padEnd(10),
62
+ `${result.ttfbMs === null ? "-" : `${result.ttfbMs} ms`}`.padStart(8),
63
+ `${result.edgeRegion ?? "-"}${result.functionRegion ? ` -> ${result.functionRegion}` : ""}`.padEnd(14),
64
+ result.url,
65
+ ];
66
+ return `${cells.join(" ")}${result.problem ? ` (${result.problem})` : ""}`;
67
+ }
68
+ export async function runEdgeProbe(requests, options = {}) {
69
+ const fetcher = options.fetch ?? globalThis.fetch;
70
+ const now = options.now ?? (() => performance.now());
71
+ const results = [];
72
+ for (const request of requests) {
73
+ const started = now();
74
+ try {
75
+ const response = await fetcher(request.url, { headers: { ...request.headers, "user-agent": "brandlab-edge-probe" }, redirect: "manual", signal: AbortSignal.timeout(30_000) });
76
+ const ttfbMs = Math.round(now() - started);
77
+ await response.body?.cancel().catch(() => undefined);
78
+ const vercelId = response.headers.get("x-vercel-id");
79
+ const regions = vercelIdRegions(vercelId);
80
+ const problem = request.expectStatus !== undefined && response.status !== request.expectStatus ? `expected ${request.expectStatus}` : undefined;
81
+ results.push({
82
+ kind: request.kind,
83
+ url: request.url,
84
+ status: response.status,
85
+ cache: response.headers.get("x-vercel-cache"),
86
+ age: response.headers.get("age"),
87
+ vercelId,
88
+ functionRegion: regions.function,
89
+ edgeRegion: regions.edge,
90
+ ttfbMs,
91
+ ...(problem ? { problem } : {}),
92
+ });
93
+ }
94
+ catch (error) {
95
+ results.push({ kind: request.kind, url: request.url, status: null, cache: null, age: null, vercelId: null, functionRegion: null, edgeRegion: null, ttfbMs: null, problem: error instanceof Error ? error.message : String(error) });
96
+ }
97
+ }
98
+ return results;
99
+ }
100
+ export async function runEdgeProbeCli(argv, io, options = {}) {
101
+ try {
102
+ const json = argv.includes("--json");
103
+ const urls = argv.filter((argument) => argument !== "--json");
104
+ const unknown = urls.find((argument) => argument.startsWith("--"));
105
+ if (unknown)
106
+ throw new Error(`Unknown option ${unknown}. Usage: brandlab-edge-probe [--json] <url>...`);
107
+ const results = await runEdgeProbe(planEdgeProbe(urls), options);
108
+ if (json)
109
+ io.stdout(`${JSON.stringify(results, null, 2)}\n`);
110
+ else
111
+ io.stdout(`${results.map(formatEdgeProbeRow).join("\n")}\n`);
112
+ return results.some((result) => result.problem) ? 1 : 0;
113
+ }
114
+ catch (error) {
115
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
116
+ return 2;
117
+ }
118
+ }
119
+ function isEntrypoint(moduleUrl, argvPath) {
120
+ if (!argvPath)
121
+ return false;
122
+ try {
123
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
129
+ if (isEntrypoint(import.meta.url, process.argv[1])) {
130
+ runEdgeProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {
131
+ process.exitCode = code;
132
+ });
133
+ }
134
+ //# sourceMappingURL=edge-probe-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-probe-cli.js","sourceRoot":"","sources":["../src/edge-probe-cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * brandlab-edge-probe (3.13.0): `npm run probe:edge`, run on a laptop, never in CI. For each fixed URL it requests the\n * page, its RSC payload and its `_tree` segment prefetch the way the Next 16 router does, then makes one GET to\n * `/api/contact` on each origin (which answers 405), and logs `x-vercel-cache`, `age`, `x-vercel-id` and the time to\n * first byte. It is the measuring stick for the origin-region benchmark (operations/shared-platform/ORIGIN_REGION.md):\n * `x-vercel-id` reads `<edge>::<function region>::<id>`, so `syd1::iad1::…` on `/api/contact` confirms a Washington\n * function. A handful of requests, never a crawl: at most 25 URLs.\n *\n * npx brandlab-edge-probe https://www.example.com.au/ https://www.example.com.au/services/blocked-drains\n * npx brandlab-edge-probe --json https://www.example.com.au/\n */\nimport { realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const EDGE_PROBE_MAX_URLS = 25;\n\nexport type EdgeProbeKind = \"page\" | \"rsc\" | \"tree\" | \"contact\";\n\nexport interface EdgeProbeRequest {\n readonly kind: EdgeProbeKind;\n readonly url: string;\n readonly headers: Readonly<Record<string, string>>;\n /** The status the request should answer; only the contact GET has one (405). */\n readonly expectStatus?: number;\n}\n\nexport interface EdgeProbeResult {\n readonly kind: EdgeProbeKind;\n readonly url: string;\n readonly status: number | null;\n readonly cache: string | null;\n readonly age: string | null;\n readonly vercelId: string | null;\n /** The function region from x-vercel-id when the request reached a function. */\n readonly functionRegion: string | null;\n readonly edgeRegion: string | null;\n readonly ttfbMs: number | null;\n readonly problem?: string;\n}\n\n/** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */\nexport function planEdgeProbe(urls: readonly string[]): EdgeProbeRequest[] {\n if (urls.length === 0) throw new Error(\"Name at least one URL to probe.\");\n if (urls.length > EDGE_PROBE_MAX_URLS) throw new Error(`Probe at most ${EDGE_PROBE_MAX_URLS} URLs; this is a measurement, not a crawl.`);\n const requests: EdgeProbeRequest[] = [];\n const origins: string[] = [];\n for (const raw of urls) {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`\"${raw}\" is not an absolute URL such as https://www.example.com.au/services.`);\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") throw new Error(`\"${raw}\" is not an http(s) URL.`);\n url.hash = \"\";\n const href = url.toString();\n requests.push({ kind: \"page\", url: href, headers: { accept: \"text/html\" } });\n requests.push({ kind: \"rsc\", url: href, headers: { rsc: \"1\" } });\n requests.push({ kind: \"tree\", url: href, headers: { rsc: \"1\", \"next-router-prefetch\": \"1\", \"next-router-segment-prefetch\": \"/_tree\" } });\n if (!origins.includes(url.origin)) origins.push(url.origin);\n }\n for (const origin of origins) requests.push({ kind: \"contact\", url: `${origin}/api/contact`, headers: { accept: \"application/json\" }, expectStatus: 405 });\n return requests;\n}\n\n/** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */\nexport function vercelIdRegions(vercelId: string | null): { edge: string | null; function: string | null } {\n if (!vercelId) return { edge: null, function: null };\n const parts = vercelId.split(\"::\");\n const region = (part: string | undefined) => (part && /^[a-z]{3}\\d$/.test(part) ? part : null);\n return { edge: region(parts[0]), function: parts.length >= 3 ? region(parts[1]) : null };\n}\n\n/** One result as a row of the plain-text table. */\nexport function formatEdgeProbeRow(result: EdgeProbeResult): string {\n const cells = [\n result.kind.padEnd(7),\n String(result.status ?? \"-\").padEnd(4),\n (result.cache ?? \"-\").padEnd(8),\n `age ${result.age ?? \"-\"}`.padEnd(10),\n `${result.ttfbMs === null ? \"-\" : `${result.ttfbMs} ms`}`.padStart(8),\n `${result.edgeRegion ?? \"-\"}${result.functionRegion ? ` -> ${result.functionRegion}` : \"\"}`.padEnd(14),\n result.url,\n ];\n return `${cells.join(\" \")}${result.problem ? ` (${result.problem})` : \"\"}`;\n}\n\ntype Fetcher = (url: string, init: { headers: Record<string, string>; redirect: \"manual\"; signal?: AbortSignal }) => Promise<{\n status: number;\n headers: { get(name: string): string | null };\n body?: { cancel(): Promise<void> } | null;\n}>;\n\nexport async function runEdgeProbe(requests: readonly EdgeProbeRequest[], options: { fetch?: Fetcher; now?: () => number } = {}): Promise<EdgeProbeResult[]> {\n const fetcher = options.fetch ?? (globalThis.fetch as unknown as Fetcher);\n const now = options.now ?? (() => performance.now());\n const results: EdgeProbeResult[] = [];\n for (const request of requests) {\n const started = now();\n try {\n const response = await fetcher(request.url, { headers: { ...request.headers, \"user-agent\": \"brandlab-edge-probe\" }, redirect: \"manual\", signal: AbortSignal.timeout(30_000) });\n const ttfbMs = Math.round(now() - started);\n await response.body?.cancel().catch(() => undefined);\n const vercelId = response.headers.get(\"x-vercel-id\");\n const regions = vercelIdRegions(vercelId);\n const problem = request.expectStatus !== undefined && response.status !== request.expectStatus ? `expected ${request.expectStatus}` : undefined;\n results.push({\n kind: request.kind,\n url: request.url,\n status: response.status,\n cache: response.headers.get(\"x-vercel-cache\"),\n age: response.headers.get(\"age\"),\n vercelId,\n functionRegion: regions.function,\n edgeRegion: regions.edge,\n ttfbMs,\n ...(problem ? { problem } : {}),\n });\n } catch (error) {\n results.push({ kind: request.kind, url: request.url, status: null, cache: null, age: null, vercelId: null, functionRegion: null, edgeRegion: null, ttfbMs: null, problem: error instanceof Error ? error.message : String(error) });\n }\n }\n return results;\n}\n\nexport async function runEdgeProbeCli(argv: readonly string[], io: { stdout: (text: string) => void; stderr: (text: string) => void }, options: { fetch?: Fetcher } = {}): Promise<number> {\n try {\n const json = argv.includes(\"--json\");\n const urls = argv.filter((argument) => argument !== \"--json\");\n const unknown = urls.find((argument) => argument.startsWith(\"--\"));\n if (unknown) throw new Error(`Unknown option ${unknown}. Usage: brandlab-edge-probe [--json] <url>...`);\n const results = await runEdgeProbe(planEdgeProbe(urls), options);\n if (json) io.stdout(`${JSON.stringify(results, null, 2)}\\n`);\n else io.stdout(`${results.map(formatEdgeProbeRow).join(\"\\n\")}\\n`);\n return results.some((result) => result.problem) ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 2;\n }\n}\n\nfunction isEntrypoint(moduleUrl: string, argvPath: string | undefined): 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 (isEntrypoint(import.meta.url, process.argv[1])) {\n runEdgeProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {\n process.exitCode = code;\n });\n}\n"]}
@@ -17,7 +17,10 @@ export interface EngineeringCheck {
17
17
  coverage: string;
18
18
  outstandingEvidence: readonly string[];
19
19
  }
20
- /** The Vercel function region of every Brandlab site (3.2.1): Sydney. */
20
+ /**
21
+ * The default Vercel function region (3.2.1): Sydney. From 3.13.0 a site may record another in brandlab.config.ts
22
+ * (`hosting.originRegion`) with an origin-region-v1 decision; `vercel.json` regions must equal the recorded value.
23
+ */
21
24
  export declare const BRANDLAB_FUNCTION_REGION = "syd1";
22
25
  /**
23
26
  * The Vercel ignored build step of every Brandlab site (3.3.3). Exit 0 skips a deployment, anything else builds it:
@@ -1 +1 @@
1
- {"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,yBAAyB,sGAAuG,CAAC;AAE9I,oFAAoF;AACpF,eAAO,MAAM,+BAA+B,4IAC+F,CAAC;AAE5I,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAC5D,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,0BAA0B,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AACxI,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAqBD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,iOAC6K,CAAC;AAqBzN,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAyBxE;AAwCD,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CA2B5G"}
1
+ {"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,yBAAyB,sGAAuG,CAAC;AAE9I,oFAAoF;AACpF,eAAO,MAAM,+BAA+B,4IAC+F,CAAC;AAE5I,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAC5D,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,0BAA0B,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AACxI,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAqBD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,iOAC6K,CAAC;AAqBzN,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAyBxE;AAwCD,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CA2B5G"}
@@ -4,6 +4,8 @@ import ts from 'typescript';
4
4
  import { globMatch } from './glob.js';
5
5
  import { collectBrandlabSource, createBrandlabModuleResolver, findImportedCalls, isDirectory, lineOfNode, listModuleReferences, objectPropertyValue, parseBrandlabSourceFile, walkProjectFiles, } from './source-files.js';
6
6
  import { describeCapabilities } from './static-rules.js';
7
+ import { originRegionProblems } from './origin-region.js';
8
+ import { parseProjectHosting } from './site-system/config.js';
7
9
  export const ENGINEERING_FAST_COMMANDS = ['typecheck', 'lint', 'test', 'check:unused', 'check:unused:production', 'check:standards'];
8
10
  /** The exact fast verification command from the Brandlab 3.0 target (section 6). */
9
11
  export const BRANDLAB_EXPECTED_VERIFY_SCRIPT = 'npm run check:standards && npm run typecheck && npm run lint && npm run test && npm run check:unused && npm run check:unused:production';
@@ -29,7 +31,10 @@ const CODE_OR_STYLE = /\.(?:[cm]?[jt]sx?|mdx|css)$/;
29
31
  const PASCAL_COMPONENT = /^[A-Z][A-Za-z0-9]*\.tsx$/;
30
32
  const CAMEL_MODULE = /^[a-z][A-Za-z0-9]*(?:\.d)?\.ts$/;
31
33
  const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
32
- /** The Vercel function region of every Brandlab site (3.2.1): Sydney. */
34
+ /**
35
+ * The default Vercel function region (3.2.1): Sydney. From 3.13.0 a site may record another in brandlab.config.ts
36
+ * (`hosting.originRegion`) with an origin-region-v1 decision; `vercel.json` regions must equal the recorded value.
37
+ */
33
38
  export const BRANDLAB_FUNCTION_REGION = 'syd1';
34
39
  /**
35
40
  * The Vercel ignored build step of every Brandlab site (3.3.3). Exit 0 skips a deployment, anything else builds it:
@@ -79,7 +84,7 @@ export function checkEngineeringStructure(root) {
79
84
  inspectServerBoundaries(root, capabilities, source, add);
80
85
  inspectTests(root, capabilities, add);
81
86
  inspectToolPresets(root, add);
82
- inspectDelivery(root, add);
87
+ inspectDelivery(root, add, config);
83
88
  return { status: findings.some(finding => finding.severity === 'blocker') ? 'blocked' : 'pass', findings,
84
89
  coverage: 'Static layout, naming, command, dependency-pin, tool-preset, CI, deployment and documentation structure only. This is not engineering-standard completion.',
85
90
  outstandingEvidence: ['Usage and deletion review including assets, configuration and dynamic references', 'Business data and component/style ownership review', 'Clean install, fast checks and production build', 'Full discovered technical URL estate', 'All distinct page families and important browser journeys', 'Exact-source protected Preview', 'Reporting/Ads provider cutover and authorised production verification'] };
@@ -637,12 +642,15 @@ function inspectToolPresets(root, add) {
637
642
  }
638
643
  }
639
644
  // --- CI, dependencies, Vercel and docs (section 6) ---------------------------
640
- function inspectDelivery(root, add) {
645
+ /** A day before any decision, so a recorded decision's reviewBy never counts as passed. */
646
+ const ORIGIN_DECISION_NEVER_LAPSED = '0000-01-01';
647
+ function inspectDelivery(root, add, config) {
641
648
  const workflowDirectory = join(root, '.github/workflows');
642
649
  const workflows = isDirectory(workflowDirectory) ? readdirSync(workflowDirectory).filter(name => /\.ya?ml$/.test(name)).sort() : [];
643
- const callers = workflows.filter(name => /uses\s*:\s*['"]?RomieDandan\/brandlab\/\.github\/workflows\/verify-next\.yml@/.test(readFileSync(join(workflowDirectory, name), 'utf8')));
650
+ // CI v2 (verify-next-v2.yml, 3.13.0) is the same verifier with draft, ready-for-review and merge depths.
651
+ const callers = workflows.filter(name => /uses\s*:\s*['"]?RomieDandan\/brandlab\/\.github\/workflows\/verify-next(?:-v2)?\.yml@/.test(readFileSync(join(workflowDirectory, name), 'utf8')));
644
652
  if (callers.length !== 1 || callers[0] !== 'verify.yml') {
645
- add('.github/workflows/verify.yml', 'ci-workflow', `Exactly one .github/workflows/verify.yml calls RomieDandan/brandlab/.github/workflows/verify-next.yml at the fleet SHA (found ${callers.length ? callers.join(', ') : 'none'}).`);
653
+ add('.github/workflows/verify.yml', 'ci-workflow', `Exactly one .github/workflows/verify.yml calls RomieDandan/brandlab/.github/workflows/verify-next.yml (or verify-next-v2.yml) at the fleet SHA (found ${callers.length ? callers.join(', ') : 'none'}).`);
646
654
  }
647
655
  for (const path of ['.github/dependabot.yml', '.github/dependabot.yaml', 'renovate.json', 'renovate.json5', '.github/renovate.json', '.renovaterc', '.renovaterc.json']) {
648
656
  if (existsSync(join(root, path)))
@@ -658,15 +666,12 @@ function inspectDelivery(root, add) {
658
666
  }
659
667
  if (installCommand !== 'npm ci')
660
668
  add('vercel.json', 'vercel-install', 'vercel.json sets installCommand to "npm ci".');
661
- let regions;
662
- try {
663
- regions = vercel === null ? undefined : JSON.parse(vercel).regions;
664
- }
665
- catch {
666
- regions = undefined;
667
- }
668
- if (!Array.isArray(regions) || regions.length !== 1 || regions[0] !== BRANDLAB_FUNCTION_REGION) {
669
- add('vercel.json', 'vercel-region', `vercel.json runs the server functions in Sydney: "regions": ["${BRANDLAB_FUNCTION_REGION}"] (found ${regions === undefined ? 'no regions' : JSON.stringify(regions)}). The visitors are in Australia; a Washington (iad1) function adds a Pacific round trip to every request it serves.`);
669
+ // W5 (3.13.0): the regions equal the recorded origin region, Sydney unless an origin-region-v1 decision says otherwise.
670
+ // This check runs inside every site's `npm run verify`, so it never judges the date: a lapsed reviewBy must not stop
671
+ // a build, a fix or a rollback. The lapse fails only the ready gate's W5 (as site-core's config validation does).
672
+ const hosting = config === null ? undefined : parseProjectHosting(config);
673
+ for (const problem of originRegionProblems({ hosting, vercelJson: vercel, today: ORIGIN_DECISION_NEVER_LAPSED })) {
674
+ add(problem.startsWith('vercel.json') ? 'vercel.json' : 'brandlab.config.ts', 'vercel-region', problem);
670
675
  }
671
676
  let ignoreCommand;
672
677
  try {