@brandlab-digital/conformance 3.12.0 → 3.13.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.
- package/README.md +32 -1
- package/dist/current-release.js +6 -6
- package/dist/current-release.js.map +1 -1
- package/dist/edge-probe-cli.d.ts +57 -0
- package/dist/edge-probe-cli.d.ts.map +1 -0
- package/dist/edge-probe-cli.js +134 -0
- package/dist/edge-probe-cli.js.map +1 -0
- package/dist/engineering.d.ts +4 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +19 -14
- package/dist/engineering.js.map +1 -1
- package/dist/estate-sample.d.ts +60 -0
- package/dist/estate-sample.d.ts.map +1 -0
- package/dist/estate-sample.js +144 -0
- package/dist/estate-sample.js.map +1 -0
- package/dist/estate.d.ts +17 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +47 -10
- package/dist/estate.js.map +1 -1
- package/dist/origin-region.d.ts +14 -0
- package/dist/origin-region.d.ts.map +1 -0
- package/dist/origin-region.js +46 -0
- package/dist/origin-region.js.map +1 -0
- package/dist/prefetch-probe-cli.d.ts +36 -0
- package/dist/prefetch-probe-cli.d.ts.map +1 -0
- package/dist/prefetch-probe-cli.js +163 -0
- package/dist/prefetch-probe-cli.js.map +1 -0
- package/dist/site-system/checks.d.ts +36 -0
- package/dist/site-system/checks.d.ts.map +1 -1
- package/dist/site-system/checks.js +6 -0
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/config.d.ts +27 -0
- package/dist/site-system/config.d.ts.map +1 -1
- package/dist/site-system/config.js +50 -1
- package/dist/site-system/config.js.map +1 -1
- package/dist/site-system/index.d.ts +1 -1
- package/dist/site-system/index.d.ts.map +1 -1
- package/dist/site-system/index.js +1 -1
- package/dist/site-system/index.js.map +1 -1
- package/dist/site-system/journeys/plan.d.ts +9 -2
- package/dist/site-system/journeys/plan.d.ts.map +1 -1
- package/dist/site-system/journeys/plan.js +11 -1
- package/dist/site-system/journeys/plan.js.map +1 -1
- package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
- package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch-measure.js +94 -0
- package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
- package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
- package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch-policy.js +152 -0
- package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
- package/dist/site-system/journeys/prefetch.d.ts +3 -0
- package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch.js +37 -0
- package/dist/site-system/journeys/prefetch.js.map +1 -0
- package/dist/site-system/journeys/variants.d.ts +34 -0
- package/dist/site-system/journeys/variants.d.ts.map +1 -0
- package/dist/site-system/journeys/variants.js +139 -0
- package/dist/site-system/journeys/variants.js.map +1 -0
- package/dist/site-system/rule-modules.d.ts.map +1 -1
- package/dist/site-system/rule-modules.js +5 -0
- package/dist/site-system/rule-modules.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +3 -0
- package/dist/site-system/rules/full/common.d.ts.map +1 -1
- package/dist/site-system/rules/full/common.js.map +1 -1
- package/dist/site-system/rules/full/estate.d.ts +4 -0
- package/dist/site-system/rules/full/estate.d.ts.map +1 -1
- package/dist/site-system/rules/full/estate.js +11 -2
- package/dist/site-system/rules/full/estate.js.map +1 -1
- package/dist/site-system/rules/full/index.d.ts +2 -1
- package/dist/site-system/rules/full/index.d.ts.map +1 -1
- package/dist/site-system/rules/full/index.js +14 -7
- package/dist/site-system/rules/full/index.js.map +1 -1
- package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
- package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
- package/dist/site-system/rules/full/w3-refresh.js +70 -0
- package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
- package/dist/site-system/rules/guards/index.d.ts +22 -0
- package/dist/site-system/rules/guards/index.d.ts.map +1 -0
- package/dist/site-system/rules/guards/index.js +109 -0
- package/dist/site-system/rules/guards/index.js.map +1 -0
- package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
- package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
- package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
- package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
- package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
- package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
- package/dist/site-system/rules/guards/w9-params.js +84 -0
- package/dist/site-system/rules/guards/w9-params.js.map +1 -0
- package/dist/site-system-e2e.d.ts +1 -0
- package/dist/site-system-e2e.d.ts.map +1 -1
- package/dist/site-system-e2e.js +8 -2
- package/dist/site-system-e2e.js.map +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* brandlab-prefetch-probe (3.13.0): measures one page's Next prefetches, run locally against a URL (a local
|
|
4
|
+
* production build, a protected preview or a live page). For each viewport it lists the pages prefetched on landing
|
|
5
|
+
* and after a full scroll, with their compressed and decoded bytes and the edge cache result, and says whether the
|
|
6
|
+
* page would pass W2 (prefetch by link role).
|
|
7
|
+
*
|
|
8
|
+
* npx brandlab-prefetch-probe https://www.example.com.au/areas/hills-district
|
|
9
|
+
* npx brandlab-prefetch-probe http://localhost:3000/ --viewport 390x844 --json
|
|
10
|
+
*
|
|
11
|
+
* It loads the page once per viewport in headless Chromium (`@playwright/test`, an optional peer) and lets no request
|
|
12
|
+
* leave for another site, so analytics never counts it. It reads one page; it never crawls.
|
|
13
|
+
*/
|
|
14
|
+
import { realpathSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { measurePrefetch } from "./site-system/journeys/prefetch-measure.js";
|
|
18
|
+
import { evaluatePrefetch } from "./site-system/journeys/prefetch-policy.js";
|
|
19
|
+
export const PROBE_DEFAULT_VIEWPORTS = Object.freeze([
|
|
20
|
+
{ width: 390, height: 844 },
|
|
21
|
+
{ width: 1440, height: 900 },
|
|
22
|
+
]);
|
|
23
|
+
const HELP = `brandlab-prefetch-probe <url> [--viewport WIDTHxHEIGHT ...] [--json] [--strict]
|
|
24
|
+
|
|
25
|
+
Lists the pages one page prefetches on landing and after a full scroll, with bytes, at 390 x 844 and 1440 x 900
|
|
26
|
+
(or the viewports named), and whether it would pass W2. --strict exits 1 when it would not.`;
|
|
27
|
+
const MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Mobile Safari/537.36";
|
|
28
|
+
export function parseProbeArgs(argv) {
|
|
29
|
+
let url;
|
|
30
|
+
const viewports = [];
|
|
31
|
+
let json = false;
|
|
32
|
+
let strict = false;
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const argument = argv[index];
|
|
35
|
+
if (argument === "--json")
|
|
36
|
+
json = true;
|
|
37
|
+
else if (argument === "--strict")
|
|
38
|
+
strict = true;
|
|
39
|
+
else if (argument === "--viewport") {
|
|
40
|
+
const value = argv[++index] ?? "";
|
|
41
|
+
const match = /^(\d{3,4})x(\d{3,4})$/.exec(value);
|
|
42
|
+
if (!match)
|
|
43
|
+
throw new Error(`--viewport takes WIDTHxHEIGHT, for example 390x844 (got ${JSON.stringify(value)}).`);
|
|
44
|
+
viewports.push({ width: Number(match[1]), height: Number(match[2]) });
|
|
45
|
+
}
|
|
46
|
+
else if (argument.startsWith("--"))
|
|
47
|
+
throw new Error(`Unknown option ${argument}.`);
|
|
48
|
+
else if (url)
|
|
49
|
+
throw new Error("Name one page URL.");
|
|
50
|
+
else
|
|
51
|
+
url = argument;
|
|
52
|
+
}
|
|
53
|
+
if (!url)
|
|
54
|
+
throw new Error("Name the page URL to probe.");
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(url);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`${url} is not a full URL; include http:// or https://.`);
|
|
61
|
+
}
|
|
62
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
63
|
+
throw new Error(`${url} is not an http or https URL.`);
|
|
64
|
+
return { url: parsed.href, viewports: viewports.length > 0 ? viewports : [...PROBE_DEFAULT_VIEWPORTS], json, strict };
|
|
65
|
+
}
|
|
66
|
+
function kilobytes(bytes) {
|
|
67
|
+
return `${Math.round(bytes / 102.4) / 10} KB`;
|
|
68
|
+
}
|
|
69
|
+
/** Edge cache results by count, for example "HIT 3, MISS 9". */
|
|
70
|
+
export function cacheSummary(requests) {
|
|
71
|
+
const counts = new Map();
|
|
72
|
+
for (const request of requests)
|
|
73
|
+
counts.set(request.cache ?? "none", (counts.get(request.cache ?? "none") ?? 0) + 1);
|
|
74
|
+
return [...counts].map(([cache, count]) => `${cache} ${count}`).join(", ") || "none";
|
|
75
|
+
}
|
|
76
|
+
/** The plain-text report: per viewport, the counts, bytes, pages and the W2 result. */
|
|
77
|
+
export function formatProbeReport(url, results) {
|
|
78
|
+
const lines = [`Prefetch probe: ${url}`];
|
|
79
|
+
for (const { viewport, measurement, verdict } of results) {
|
|
80
|
+
const scrollOnly = measurement.scrolled.filter((page) => !measurement.landing.includes(page));
|
|
81
|
+
lines.push("", `${viewport}`, ` On landing: ${measurement.landing.length} page(s)${measurement.landing.length ? `: ${measurement.landing.join(", ")}` : ""}`, ` After a full scroll: ${measurement.scrolled.length} page(s)${scrollOnly.length ? `; added ${scrollOnly.join(", ")}` : ""}`, ` Bytes: ${kilobytes(measurement.bytes.prefetchCompressed)} compressed, ${kilobytes(measurement.bytes.prefetchDecoded)} decoded, in ${measurement.requests.length} request(s); the page's HTML ${kilobytes(measurement.bytes.htmlCompressed)} compressed`, ` Edge cache: ${cacheSummary(measurement.requests)}`, verdict.failures.length === 0 ? " W2: would pass" : ` W2: would fail\n${verdict.failures.map((failure) => ` - ${failure}`).join("\n")}`, ...verdict.warnings.map((warning) => ` ${warning}`));
|
|
82
|
+
}
|
|
83
|
+
return `${lines.join("\n")}\n`;
|
|
84
|
+
}
|
|
85
|
+
/** The JSON report: the same results, with each request. */
|
|
86
|
+
export function probeJson(url, results) {
|
|
87
|
+
return `${JSON.stringify({ url, viewports: results.map(({ viewport, measurement, verdict }) => ({ viewport, ...measurement, verdict })) }, null, 2)}\n`;
|
|
88
|
+
}
|
|
89
|
+
async function loadChromium() {
|
|
90
|
+
const specifier = "@playwright/test";
|
|
91
|
+
try {
|
|
92
|
+
return (await import(specifier)).chromium;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error("brandlab-prefetch-probe needs @playwright/test with Chromium: npm install --save-dev @playwright/test && npx playwright install chromium");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function probePrefetch(options, chromium) {
|
|
99
|
+
const browser = await (chromium ?? (await loadChromium())).launch({ headless: true });
|
|
100
|
+
const target = new URL(options.url);
|
|
101
|
+
const results = [];
|
|
102
|
+
try {
|
|
103
|
+
for (const viewport of options.viewports) {
|
|
104
|
+
const mobile = viewport.width < 768;
|
|
105
|
+
const context = await browser.newContext({
|
|
106
|
+
viewport: { ...viewport },
|
|
107
|
+
isMobile: mobile,
|
|
108
|
+
hasTouch: mobile,
|
|
109
|
+
serviceWorkers: "block",
|
|
110
|
+
...(mobile ? { userAgent: MOBILE_USER_AGENT } : {}),
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
// Only the site under test: analytics, tags and other sites never see the probe.
|
|
114
|
+
await context.route("**/*", (route) => (new URL(route.request().url()).origin === target.origin ? route.continue() : route.abort()));
|
|
115
|
+
const page = await context.newPage();
|
|
116
|
+
const measurement = await measurePrefetch(page, `${target.pathname}${target.search}`, {
|
|
117
|
+
open: (opened) => opened.goto(target.href, { waitUntil: "load", timeout: 60_000 }),
|
|
118
|
+
});
|
|
119
|
+
const label = `${viewport.width} x ${viewport.height}`;
|
|
120
|
+
results.push({ viewport: label, measurement, verdict: evaluatePrefetch(measurement, label) });
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await context.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await browser.close();
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
export async function runPrefetchProbeCli(argv, io, probe = probePrefetch) {
|
|
133
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
134
|
+
io.stdout(`${HELP}\n`);
|
|
135
|
+
return argv.length === 0 ? 1 : 0;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const options = parseProbeArgs(argv);
|
|
139
|
+
const results = await probe(options);
|
|
140
|
+
io.stdout(options.json ? probeJson(options.url, results) : formatProbeReport(options.url, results));
|
|
141
|
+
return options.strict && results.some((result) => result.verdict.failures.length > 0) ? 1 : 0;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isEntrypoint(moduleUrl, argvPath) {
|
|
149
|
+
if (!argvPath)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
159
|
+
runPrefetchProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {
|
|
160
|
+
process.exitCode = code;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=prefetch-probe-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefetch-probe-cli.js","sourceRoot":"","sources":["../src/prefetch-probe-cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * brandlab-prefetch-probe (3.13.0): measures one page's Next prefetches, run locally against a URL (a local\n * production build, a protected preview or a live page). For each viewport it lists the pages prefetched on landing\n * and after a full scroll, with their compressed and decoded bytes and the edge cache result, and says whether the\n * page would pass W2 (prefetch by link role).\n *\n * npx brandlab-prefetch-probe https://www.example.com.au/areas/hills-district\n * npx brandlab-prefetch-probe http://localhost:3000/ --viewport 390x844 --json\n *\n * It loads the page once per viewport in headless Chromium (`@playwright/test`, an optional peer) and lets no request\n * leave for another site, so analytics never counts it. It reads one page; it never crawls.\n */\nimport { realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { measurePrefetch, type PrefetchMeasureResult } from \"./site-system/journeys/prefetch-measure.js\";\nimport { evaluatePrefetch, type PrefetchVerdict } from \"./site-system/journeys/prefetch-policy.js\";\n\nexport interface ProbeViewport {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ProbeOptions {\n readonly url: string;\n readonly viewports: readonly ProbeViewport[];\n readonly json: boolean;\n /** Exit 1 when the page would fail W2. */\n readonly strict: boolean;\n}\n\nexport interface ProbeViewportResult {\n readonly viewport: string;\n readonly measurement: PrefetchMeasureResult;\n readonly verdict: PrefetchVerdict;\n}\n\nexport const PROBE_DEFAULT_VIEWPORTS: readonly ProbeViewport[] = Object.freeze([\n { width: 390, height: 844 },\n { width: 1440, height: 900 },\n]);\n\nconst HELP = `brandlab-prefetch-probe <url> [--viewport WIDTHxHEIGHT ...] [--json] [--strict]\n\nLists the pages one page prefetches on landing and after a full scroll, with bytes, at 390 x 844 and 1440 x 900\n(or the viewports named), and whether it would pass W2. --strict exits 1 when it would not.`;\n\nconst MOBILE_USER_AGENT = \"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Mobile Safari/537.36\";\n\nexport function parseProbeArgs(argv: readonly string[]): ProbeOptions {\n let url: string | undefined;\n const viewports: ProbeViewport[] = [];\n let json = false;\n let strict = false;\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index]!;\n if (argument === \"--json\") json = true;\n else if (argument === \"--strict\") strict = true;\n else if (argument === \"--viewport\") {\n const value = argv[++index] ?? \"\";\n const match = /^(\\d{3,4})x(\\d{3,4})$/.exec(value);\n if (!match) throw new Error(`--viewport takes WIDTHxHEIGHT, for example 390x844 (got ${JSON.stringify(value)}).`);\n viewports.push({ width: Number(match[1]), height: Number(match[2]) });\n } else if (argument.startsWith(\"--\")) throw new Error(`Unknown option ${argument}.`);\n else if (url) throw new Error(\"Name one page URL.\");\n else url = argument;\n }\n if (!url) throw new Error(\"Name the page URL to probe.\");\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`${url} is not a full URL; include http:// or https://.`);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") throw new Error(`${url} is not an http or https URL.`);\n return { url: parsed.href, viewports: viewports.length > 0 ? viewports : [...PROBE_DEFAULT_VIEWPORTS], json, strict };\n}\n\nfunction kilobytes(bytes: number): string {\n return `${Math.round(bytes / 102.4) / 10} KB`;\n}\n\n/** Edge cache results by count, for example \"HIT 3, MISS 9\". */\nexport function cacheSummary(requests: PrefetchMeasureResult[\"requests\"]): string {\n const counts = new Map<string, number>();\n for (const request of requests) counts.set(request.cache ?? \"none\", (counts.get(request.cache ?? \"none\") ?? 0) + 1);\n return [...counts].map(([cache, count]) => `${cache} ${count}`).join(\", \") || \"none\";\n}\n\n/** The plain-text report: per viewport, the counts, bytes, pages and the W2 result. */\nexport function formatProbeReport(url: string, results: readonly ProbeViewportResult[]): string {\n const lines = [`Prefetch probe: ${url}`];\n for (const { viewport, measurement, verdict } of results) {\n const scrollOnly = measurement.scrolled.filter((page) => !measurement.landing.includes(page));\n lines.push(\n \"\",\n `${viewport}`,\n ` On landing: ${measurement.landing.length} page(s)${measurement.landing.length ? `: ${measurement.landing.join(\", \")}` : \"\"}`,\n ` After a full scroll: ${measurement.scrolled.length} page(s)${scrollOnly.length ? `; added ${scrollOnly.join(\", \")}` : \"\"}`,\n ` Bytes: ${kilobytes(measurement.bytes.prefetchCompressed)} compressed, ${kilobytes(measurement.bytes.prefetchDecoded)} decoded, in ${measurement.requests.length} request(s); the page's HTML ${kilobytes(measurement.bytes.htmlCompressed)} compressed`,\n ` Edge cache: ${cacheSummary(measurement.requests)}`,\n verdict.failures.length === 0 ? \" W2: would pass\" : ` W2: would fail\\n${verdict.failures.map((failure) => ` - ${failure}`).join(\"\\n\")}`,\n ...verdict.warnings.map((warning) => ` ${warning}`),\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** The JSON report: the same results, with each request. */\nexport function probeJson(url: string, results: readonly ProbeViewportResult[]): string {\n return `${JSON.stringify({ url, viewports: results.map(({ viewport, measurement, verdict }) => ({ viewport, ...measurement, verdict })) }, null, 2)}\\n`;\n}\n\ntype Chromium = typeof import(\"@playwright/test\").chromium;\n\nasync function loadChromium(): Promise<Chromium> {\n const specifier: string = \"@playwright/test\";\n try {\n return ((await import(specifier)) as { chromium: Chromium }).chromium;\n } catch {\n throw new Error(\"brandlab-prefetch-probe needs @playwright/test with Chromium: npm install --save-dev @playwright/test && npx playwright install chromium\");\n }\n}\n\nexport async function probePrefetch(options: ProbeOptions, chromium?: Chromium): Promise<ProbeViewportResult[]> {\n const browser = await (chromium ?? (await loadChromium())).launch({ headless: true });\n const target = new URL(options.url);\n const results: ProbeViewportResult[] = [];\n try {\n for (const viewport of options.viewports) {\n const mobile = viewport.width < 768;\n const context = await browser.newContext({\n viewport: { ...viewport },\n isMobile: mobile,\n hasTouch: mobile,\n serviceWorkers: \"block\",\n ...(mobile ? { userAgent: MOBILE_USER_AGENT } : {}),\n });\n try {\n // Only the site under test: analytics, tags and other sites never see the probe.\n await context.route(\"**/*\", (route) => (new URL(route.request().url()).origin === target.origin ? route.continue() : route.abort()));\n const page = await context.newPage();\n const measurement = await measurePrefetch(page, `${target.pathname}${target.search}`, {\n open: (opened) => opened.goto(target.href, { waitUntil: \"load\", timeout: 60_000 }),\n });\n const label = `${viewport.width} x ${viewport.height}`;\n results.push({ viewport: label, measurement, verdict: evaluatePrefetch(measurement, label) });\n } finally {\n await context.close();\n }\n }\n } finally {\n await browser.close();\n }\n return results;\n}\n\ninterface CliIo {\n stdout(value: string): void;\n stderr(value: string): void;\n}\n\nexport async function runPrefetchProbeCli(argv: readonly string[], io: CliIo, probe: typeof probePrefetch = probePrefetch): Promise<number> {\n if (argv.length === 0 || argv.includes(\"--help\") || argv.includes(\"-h\")) {\n io.stdout(`${HELP}\\n`);\n return argv.length === 0 ? 1 : 0;\n }\n try {\n const options = parseProbeArgs(argv);\n const results = await probe(options);\n io.stdout(options.json ? probeJson(options.url, results) : formatProbeReport(options.url, results));\n return options.strict && results.some((result) => result.verdict.failures.length > 0) ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\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 runPrefetchProbeCli(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"]}
|
|
@@ -188,6 +188,24 @@ export declare const SITE_SYSTEM_CHECKS: readonly [{
|
|
|
188
188
|
readonly from: "3.8.0";
|
|
189
189
|
readonly kind: "rendered";
|
|
190
190
|
readonly summary: "No kilometre figures, \"km from\" or \"based in\" in hero copy.";
|
|
191
|
+
}, {
|
|
192
|
+
readonly id: "W2";
|
|
193
|
+
readonly tier: "shell";
|
|
194
|
+
readonly from: "3.13.0";
|
|
195
|
+
readonly kind: "journey";
|
|
196
|
+
readonly summary: "Prefetch by link role at 390 x 844 and 1440 x 900: at most 8 pages on landing and 12 after a full scroll; none from footer, area, nearby, map or finder links, hidden links or the current page; prefetch bytes above twice the page's HTML warn.";
|
|
197
|
+
}, {
|
|
198
|
+
readonly id: "W5";
|
|
199
|
+
readonly tier: "shell";
|
|
200
|
+
readonly from: "3.13.0";
|
|
201
|
+
readonly kind: "static";
|
|
202
|
+
readonly summary: "vercel.json regions equal hosting.originRegion (syd1 by default); any other region has an origin-region-v1 decision whose reviewBy has not passed.";
|
|
203
|
+
}, {
|
|
204
|
+
readonly id: "W10";
|
|
205
|
+
readonly tier: "shell";
|
|
206
|
+
readonly from: "3.13.0";
|
|
207
|
+
readonly kind: "rendered";
|
|
208
|
+
readonly summary: "robots.txt and the recorded firewall rules leave every search and answer crawler open; no Crawl-delay for them.";
|
|
191
209
|
}, {
|
|
192
210
|
readonly id: "C1";
|
|
193
211
|
readonly tier: "enquiry";
|
|
@@ -350,12 +368,30 @@ export declare const SITE_SYSTEM_CHECKS: readonly [{
|
|
|
350
368
|
readonly from: "3.12.0";
|
|
351
369
|
readonly kind: "static";
|
|
352
370
|
readonly summary: "No hand-written per-service or per-suburb files; no dynamic route with zero pages.";
|
|
371
|
+
}, {
|
|
372
|
+
readonly id: "W9";
|
|
373
|
+
readonly tier: "full";
|
|
374
|
+
readonly from: "3.13.0";
|
|
375
|
+
readonly kind: "journey";
|
|
376
|
+
readonly summary: "URL variants: an unknown slug in each family answers the shared 404, the uppercase slug the same 404 or one permanent redirect to its lowercase form; a tracking query serves the same cached page as the clean URL.";
|
|
377
|
+
}, {
|
|
378
|
+
readonly id: "W9-params";
|
|
379
|
+
readonly tier: "full";
|
|
380
|
+
readonly from: "3.13.0";
|
|
381
|
+
readonly kind: "static";
|
|
382
|
+
readonly summary: "Pages under a dynamic route never read searchParams, so a query string cannot make an estate page dynamic.";
|
|
353
383
|
}, {
|
|
354
384
|
readonly id: "LOC-INDEX";
|
|
355
385
|
readonly tier: "full";
|
|
356
386
|
readonly from: "3.12.0";
|
|
357
387
|
readonly kind: "static";
|
|
358
388
|
readonly summary: "No location route is noindex or left out of the sitemap unless the recorded wave schedule dates it (Romie's 20 Oct decision: every suburb page stays indexed).";
|
|
389
|
+
}, {
|
|
390
|
+
readonly id: "W3";
|
|
391
|
+
readonly tier: "full";
|
|
392
|
+
readonly from: "3.13.0";
|
|
393
|
+
readonly kind: "static";
|
|
394
|
+
readonly summary: "No estate family (suburb, service-in-suburb, region, council) refreshes more often than every 7 days unless siteSystem.refreshExceptions records the reason and a decision id.";
|
|
359
395
|
}, {
|
|
360
396
|
readonly id: "D4";
|
|
361
397
|
readonly tier: "new-build";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/site-system/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,iBAAiB,uCAAwC,CAAC;AACvE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,kFAAkF;AAClF,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,WAAW,CAAC;AAEhE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,KAAK,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/site-system/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,iBAAiB,uCAAwC,CAAC;AACvE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,kFAAkF;AAClF,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG,WAAW,CAAC;AAEhE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,KAAK,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsEgB,CAAC;AAEhD,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAI1E,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC,EACpD,IAAI,EAAE;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAClE,OAAO,CAIT;AAED,oGAAoG;AACpG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAAC;IACpD,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B"}
|
|
@@ -43,6 +43,9 @@ export const SITE_SYSTEM_CHECKS = [
|
|
|
43
43
|
{ id: "X1", tier: "shell", from: "3.8.0", kind: "record", summary: "Legacy URLs answer 200 or one redirect hop; earning-URL depth, titles and dollar figures kept." },
|
|
44
44
|
{ id: "B1", tier: "shell", from: "3.8.0", kind: "rendered", summary: "Lab budgets: JavaScript, third-party requests, hero image and HTML within budget; no unrecorded chat widget, pixel, heatmap or tag manager." },
|
|
45
45
|
{ id: "L10-hero", tier: "shell", from: "3.8.0", kind: "rendered", summary: 'No kilometre figures, "km from" or "based in" in hero copy.' },
|
|
46
|
+
{ id: "W2", tier: "shell", from: "3.13.0", kind: "journey", summary: "Prefetch by link role at 390 x 844 and 1440 x 900: at most 8 pages on landing and 12 after a full scroll; none from footer, area, nearby, map or finder links, hidden links or the current page; prefetch bytes above twice the page's HTML warn." },
|
|
47
|
+
{ id: "W5", tier: "shell", from: "3.13.0", kind: "static", summary: "vercel.json regions equal hosting.originRegion (syd1 by default); any other region has an origin-region-v1 decision whose reviewBy has not passed." },
|
|
48
|
+
{ id: "W10", tier: "shell", from: "3.13.0", kind: "rendered", summary: "robots.txt and the recorded firewall rules leave every search and answer crawler open; no Crawl-delay for them." },
|
|
46
49
|
// enquiry: the form, thank-you page, reviews, proof strip, footer, CTA band, contact page, prices, media, density
|
|
47
50
|
{ id: "C1", tier: "enquiry", from: "3.8.0", kind: "journey", summary: "Money pages: the form in the first section; at 390 x 844 the H1, a call and a quote action in the first viewport; at 1440 x 900 the form." },
|
|
48
51
|
{ id: "C2", tier: "enquiry", from: "3.8.0", kind: "rendered", summary: 'Form layout and labels; phone and email required; no helper or reassurance lines, no "(required)", no job select.' },
|
|
@@ -72,7 +75,10 @@ export const SITE_SYSTEM_CHECKS = [
|
|
|
72
75
|
{ id: "L9", tier: "full", from: "3.12.0", kind: "rendered", summary: "Data contract on service-in-suburb pages: typical prices or the cost-guide link, reviews or their fallback, the quote path (form with the suburb prefilled, phone, call bar); the nearby job card or none." },
|
|
73
76
|
{ id: "L10", tier: "full", from: "3.12.0", kind: "rendered", summary: 'No kilometre figures, "km from", "based in" or year stamps as local detail in location copy.' },
|
|
74
77
|
{ id: "S4", tier: "full", from: "3.12.0", kind: "static", summary: "No hand-written per-service or per-suburb files; no dynamic route with zero pages." },
|
|
78
|
+
{ id: "W9", tier: "full", from: "3.13.0", kind: "journey", summary: "URL variants: an unknown slug in each family answers the shared 404, the uppercase slug the same 404 or one permanent redirect to its lowercase form; a tracking query serves the same cached page as the clean URL." },
|
|
79
|
+
{ id: "W9-params", tier: "full", from: "3.13.0", kind: "static", summary: "Pages under a dynamic route never read searchParams, so a query string cannot make an estate page dynamic." },
|
|
75
80
|
{ id: "LOC-INDEX", tier: "full", from: "3.12.0", kind: "static", summary: "No location route is noindex or left out of the sitemap unless the recorded wave schedule dates it (Romie's 20 Oct decision: every suburb page stays indexed)." },
|
|
81
|
+
{ id: "W3", tier: "full", from: "3.13.0", kind: "static", summary: "No estate family (suburb, service-in-suburb, region, council) refreshes more often than every 7 days unless siteSystem.refreshExceptions records the reason and a decision id." },
|
|
76
82
|
// new builds and redesigns
|
|
77
83
|
{ id: "D4", tier: "new-build", from: "3.8.0", kind: "static", summary: "Same-trade distinctness: at least four of seven look levers differ from every listed competitor (advisory in 3.8.0, blocking from 3.9.0)." },
|
|
78
84
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/site-system/checks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Site System v2 ready gate: every check, the adoption tier it blocks at and the release that brings it.\n *\n * A site declares `siteSystem: { tier }` in brandlab.config.ts. Checks of that tier and the tiers below it block;\n * checks of higher tiers report without blocking. A check blocks only from the release named here, so new checks\n * arrive with version bumps and never turn the fleet red at once. Holdouts use the project's Romie-owned\n * `exceptions` record. D6 (leave reviews as they are) removes every review-exclusion clause; D7 (no call tracking)\n * means no number-swap check.\n *\n * `kind` says where a check runs:\n * - `journey`: the Playwright site-system journeys (`defineBrandlabSiteSystemTests`), in the site's e2e run;\n * - `rendered`: rules over the built site's server HTML and assets (`brandlab-ready` reads them);\n * - `static`: source and data rules (`brandlab-verify` and `brandlab-ready`);\n * - `record`: launch-day and human steps recorded with `brandlab-ready record <id> --evidence <ref>`.\n */\n\nexport const SITE_SYSTEM_TIERS = [\"shell\", \"enquiry\", \"full\"] as const;\nexport type SiteSystemTier = (typeof SITE_SYSTEM_TIERS)[number];\n\n/** The design check D4 blocks every new build and redesign, whatever its tier. */\nexport type SiteSystemCheckScope = SiteSystemTier | \"new-build\";\n\nexport type SiteSystemCheckKind = \"journey\" | \"rendered\" | \"static\" | \"record\";\n\nexport interface SiteSystemCheck {\n readonly id: string;\n readonly tier: SiteSystemCheckScope;\n /** The first release in which the check blocks at its tier. */\n readonly from: `3.${number}.0`;\n readonly kind: SiteSystemCheckKind;\n readonly summary: string;\n}\n\nexport const SITE_SYSTEM_CHECKS = [\n // shell: header, nav, call bar, placements, crawlable links, contrast and targets, call links, budgets, legal\n { id: \"N1\", tier: \"shell\", from: \"3.9.0\", kind: \"rendered\", summary: \"Required nav links, About in the first two, services before Areas, Contact last, labels from the closed list, recorded orders kept.\" },\n { id: \"N2\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every dropdown link and hub link is an <a href> in the server HTML.\" },\n { id: \"N3\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Hover opens after a short delay and a diagonal path stays open; mouse-out closes; nothing sticks open.\" },\n { id: \"N4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"A chevron click after hover keeps the panel open; a link click closes it; click, Enter and outside click work.\" },\n { id: \"N5\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Keyboard and ARIA: aria-expanded follows the panel, Arrow keys move, Escape closes and returns focus; axe passes open and closed.\" },\n { id: \"N6\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"The header stays at the top with the phone and quote visible at every desktop width; the compact band shows them from 1024 to 1279 px.\" },\n { id: \"N8\", tier: \"shell\", from: \"3.9.0\", kind: \"journey\", summary: \"The drawer is a modal dialog that traps focus and resets; the call bar uses placement fab; back-to-top appears (from 3.11.0).\" },\n { id: \"K1\", tier: \"shell\", from: \"3.9.0\", kind: \"rendered\", summary: \"Shared components in use for the parts the tier covers (data-bl present); no literal tel: outside PhoneLink.\" },\n { id: \"K3\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Text contrast 4.5:1 and non-text 3:1 in the browser.\" },\n { id: \"K4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Targets at least 24 px, and 44 px for primary actions (call, quote, menu).\" },\n { id: \"K5\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Focus is never fully hidden by the sticky header or call bar at 390 px, in Chromium and WebKit.\" },\n { id: \"K6\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Phone and quote in the same relative order on every page, in the header, hero and call bar.\" },\n { id: \"K7\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Dropdown links, accordion and tab panels and full review text are in the server HTML.\" },\n { id: \"K10\", tier: \"shell\", from: \"3.8.0\", kind: \"record\", summary: \"Adoption parity in the lenient harness: no section, link or text lost against the baseline.\" },\n { id: \"C4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Phone taps report an allowed placement (header, footer, hero, cta, fab, contact, location, page).\" },\n { id: \"C5\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Call-to-action labels come from the closed list or a recorded label.\" },\n { id: \"C9\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No interstitial overlay in the first screen.\" },\n { id: \"C10\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every tel: href is E.164 (national digits for 13, 1300 and 1800) and matches the evidence-file phone.\" },\n { id: \"E1\", tier: \"shell\", from: \"3.8.0\", kind: \"static\", summary: \"Title patterns on new or changed URLs; recorded titles unchanged.\" },\n { id: \"E2\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Exactly one H1 on every page, rendered once.\" },\n { id: \"E8\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No banned phrase from any of the three layers, environmental claims included.\" },\n { id: \"T3\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"A licence line renders wherever licensed work is advertised, with the licensee where it differs.\" },\n { id: \"T4\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No hard-coded rating or review numbers; proof claims resolve to renderable evidence.\" },\n { id: \"T11\", tier: \"shell\", from: \"3.8.0\", kind: \"static\", summary: \"No page for work over $5,000 outside the licence classes held; licence conditions carried.\" },\n { id: \"X1\", tier: \"shell\", from: \"3.8.0\", kind: \"record\", summary: \"Legacy URLs answer 200 or one redirect hop; earning-URL depth, titles and dollar figures kept.\" },\n { id: \"B1\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Lab budgets: JavaScript, third-party requests, hero image and HTML within budget; no unrecorded chat widget, pixel, heatmap or tag manager.\" },\n { id: \"L10-hero\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: 'No kilometre figures, \"km from\" or \"based in\" in hero copy.' },\n\n // enquiry: the form, thank-you page, reviews, proof strip, footer, CTA band, contact page, prices, media, density\n { id: \"C1\", tier: \"enquiry\", from: \"3.8.0\", kind: \"journey\", summary: \"Money pages: the form in the first section; at 390 x 844 the H1, a call and a quote action in the first viewport; at 1440 x 900 the form.\" },\n { id: \"C2\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: 'Form layout and labels; phone and email required; no helper or reassurance lines, no \"(required)\", no job select.' },\n { id: \"E9\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every cost guide carries at least 8 dollar figures in a renderable price state.\" },\n { id: \"T5\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Reviews from the feed, with Google's rating, count and link beside any selection; none on the contact page; no empty reviews section.\" },\n { id: \"K8\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Section contract: no empty section, one H2 each, at most one band tone, neighbours differ, proof within the first five sections.\" },\n { id: \"C7\", tier: \"enquiry\", from: \"3.10.0\", kind: \"rendered\", summary: \"Thank-you page: a reply time, a tel: link and noindex, follow.\" },\n { id: \"C8\", tier: \"enquiry\", from: \"3.10.0\", kind: \"rendered\", summary: \"Forgiving form: tolerant phone input (13, 1300 and 1800 too), autocomplete tokens, no capture on the photo input.\" },\n { id: \"C11\", tier: \"enquiry\", from: \"3.10.0\", kind: \"static\", summary: \"The test marker is stripped before delivery and reporting; /api/contact is rate-limited.\" },\n { id: \"N9\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: 'Footer content: column 1 brand, licences, socials and contact; services, areas and company columns; © name, ABN and legal links; no CTA, \"based in\", hours or guides column.' },\n { id: \"K2\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"Every rendered shared component has a skin entry; no reference-skin class ships.\" },\n { id: \"K11\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"Density: no section with more than one H2, more than 8 cards or more than 2 calls to action; measure and section padding within limits.\" },\n { id: \"K12-contact\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"The contact page is the page header, then the contact block, and nothing else.\" },\n { id: \"T6\", tier: \"enquiry\", from: \"3.11.0\", kind: \"static\", summary: \"Every image has a media.ts entry with provenance.\" },\n { id: \"T7\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"No image is the main image of two pages.\" },\n { id: \"T8\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: 'Prices: GST flag set; no \"+ GST\" or \"from $X\" teasers on cards and menus; one band per job site-wide.' },\n { id: \"T9\", tier: \"enquiry\", from: \"3.11.0\", kind: \"static\", summary: \"No image in /public carries location metadata.\" },\n\n // full: page families, location estates, section manifests, links and the data contract\n { id: \"K12\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"Section manifest: every required section rendered or skipped with a data-backed reason; conditional sections render when their data exists; canonical or recorded order.\" },\n { id: \"E5\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No orphan indexable pages, from the route catalogue and link data.\" },\n { id: \"E10\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Phrase-owner uniqueness in strategy data; no service-in-suburb page for the suburb page phrase; no service-region hub for the main trade.\" },\n { id: \"L3\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Neighbours by real distance.\" },\n { id: \"L4\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"No copy rotated by page address, no name-swapped FAQs, no other suburb's job without the nearby label.\" },\n { id: \"L6\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Location pages have 6 or more inbound links from 3 or more page types.\" },\n { id: \"L7\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Location pages are 3 clicks or fewer from home.\" },\n { id: \"L8\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"A hub per family per region.\" },\n { id: \"L9\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"Data contract on service-in-suburb pages: typical prices or the cost-guide link, reviews or their fallback, the quote path (form with the suburb prefilled, phone, call bar); the nearby job card or none.\" },\n { id: \"L10\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: 'No kilometre figures, \"km from\", \"based in\" or year stamps as local detail in location copy.' },\n { id: \"S4\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No hand-written per-service or per-suburb files; no dynamic route with zero pages.\" },\n { id: \"LOC-INDEX\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No location route is noindex or left out of the sitemap unless the recorded wave schedule dates it (Romie's 20 Oct decision: every suburb page stays indexed).\" },\n\n // new builds and redesigns\n { id: \"D4\", tier: \"new-build\", from: \"3.8.0\", kind: \"static\", summary: \"Same-trade distinctness: at least four of seven look levers differ from every listed competitor (advisory in 3.8.0, blocking from 3.9.0).\" },\n] as const satisfies readonly SiteSystemCheck[];\n\nexport type SiteSystemCheckId = (typeof SITE_SYSTEM_CHECKS)[number][\"id\"];\n\nconst TIER_RANK: Record<SiteSystemTier, number> = { shell: 0, enquiry: 1, full: 2 };\n\n/** A release as one comparable number: \"3.8.0\" < \"3.10.0\". */\nexport function siteSystemReleaseRank(version: string): number {\n const [major = 0, minor = 0, patch = 0] = version.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);\n return major * 1_000_000 + minor * 1_000 + patch;\n}\n\n/**\n * Whether a check blocks for a site at `tier` on `release`. Checks of higher tiers, and checks newer than the\n * installed release, report without blocking. D4 blocks new builds and redesigns from 3.9.0 whatever their tier.\n */\nexport function siteSystemCheckBlocks(\n check: Pick<SiteSystemCheck, \"id\" | \"tier\" | \"from\">,\n site: { tier: SiteSystemTier; release: string; newBuild?: boolean },\n): boolean {\n if (check.tier === \"new-build\") return site.newBuild === true && siteSystemReleaseRank(site.release) >= siteSystemReleaseRank(\"3.9.0\");\n if (siteSystemReleaseRank(site.release) < siteSystemReleaseRank(check.from)) return false;\n return TIER_RANK[check.tier] <= TIER_RANK[site.tier];\n}\n\n/** One result of one check, from any runner. `brandlab-ready` merges them into the ready report. */\nexport interface SiteSystemFinding {\n readonly checkId: SiteSystemCheckId;\n readonly status: \"pass\" | \"fail\" | \"not-applicable\";\n /** The page path or file the result is about, when it is about one. */\n readonly target?: string;\n /** One plain sentence: what passed or what is wrong and where. */\n readonly message: string;\n /** A warning never blocks: the check still passes and the report shows \"pass (warning)\". */\n readonly severity?: \"warning\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../../src/site-system/checks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Site System v2 ready gate: every check, the adoption tier it blocks at and the release that brings it.\n *\n * A site declares `siteSystem: { tier }` in brandlab.config.ts. Checks of that tier and the tiers below it block;\n * checks of higher tiers report without blocking. A check blocks only from the release named here, so new checks\n * arrive with version bumps and never turn the fleet red at once. Holdouts use the project's Romie-owned\n * `exceptions` record. D6 (leave reviews as they are) removes every review-exclusion clause; D7 (no call tracking)\n * means no number-swap check.\n *\n * `kind` says where a check runs:\n * - `journey`: the Playwright site-system journeys (`defineBrandlabSiteSystemTests`), in the site's e2e run;\n * - `rendered`: rules over the built site's server HTML and assets (`brandlab-ready` reads them);\n * - `static`: source and data rules (`brandlab-verify` and `brandlab-ready`);\n * - `record`: launch-day and human steps recorded with `brandlab-ready record <id> --evidence <ref>`.\n */\n\nexport const SITE_SYSTEM_TIERS = [\"shell\", \"enquiry\", \"full\"] as const;\nexport type SiteSystemTier = (typeof SITE_SYSTEM_TIERS)[number];\n\n/** The design check D4 blocks every new build and redesign, whatever its tier. */\nexport type SiteSystemCheckScope = SiteSystemTier | \"new-build\";\n\nexport type SiteSystemCheckKind = \"journey\" | \"rendered\" | \"static\" | \"record\";\n\nexport interface SiteSystemCheck {\n readonly id: string;\n readonly tier: SiteSystemCheckScope;\n /** The first release in which the check blocks at its tier. */\n readonly from: `3.${number}.0`;\n readonly kind: SiteSystemCheckKind;\n readonly summary: string;\n}\n\nexport const SITE_SYSTEM_CHECKS = [\n // shell: header, nav, call bar, placements, crawlable links, contrast and targets, call links, budgets, legal\n { id: \"N1\", tier: \"shell\", from: \"3.9.0\", kind: \"rendered\", summary: \"Required nav links, About in the first two, services before Areas, Contact last, labels from the closed list, recorded orders kept.\" },\n { id: \"N2\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every dropdown link and hub link is an <a href> in the server HTML.\" },\n { id: \"N3\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Hover opens after a short delay and a diagonal path stays open; mouse-out closes; nothing sticks open.\" },\n { id: \"N4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"A chevron click after hover keeps the panel open; a link click closes it; click, Enter and outside click work.\" },\n { id: \"N5\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Keyboard and ARIA: aria-expanded follows the panel, Arrow keys move, Escape closes and returns focus; axe passes open and closed.\" },\n { id: \"N6\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"The header stays at the top with the phone and quote visible at every desktop width; the compact band shows them from 1024 to 1279 px.\" },\n { id: \"N8\", tier: \"shell\", from: \"3.9.0\", kind: \"journey\", summary: \"The drawer is a modal dialog that traps focus and resets; the call bar uses placement fab; back-to-top appears (from 3.11.0).\" },\n { id: \"K1\", tier: \"shell\", from: \"3.9.0\", kind: \"rendered\", summary: \"Shared components in use for the parts the tier covers (data-bl present); no literal tel: outside PhoneLink.\" },\n { id: \"K3\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Text contrast 4.5:1 and non-text 3:1 in the browser.\" },\n { id: \"K4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Targets at least 24 px, and 44 px for primary actions (call, quote, menu).\" },\n { id: \"K5\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Focus is never fully hidden by the sticky header or call bar at 390 px, in Chromium and WebKit.\" },\n { id: \"K6\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Phone and quote in the same relative order on every page, in the header, hero and call bar.\" },\n { id: \"K7\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Dropdown links, accordion and tab panels and full review text are in the server HTML.\" },\n { id: \"K10\", tier: \"shell\", from: \"3.8.0\", kind: \"record\", summary: \"Adoption parity in the lenient harness: no section, link or text lost against the baseline.\" },\n { id: \"C4\", tier: \"shell\", from: \"3.8.0\", kind: \"journey\", summary: \"Phone taps report an allowed placement (header, footer, hero, cta, fab, contact, location, page).\" },\n { id: \"C5\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Call-to-action labels come from the closed list or a recorded label.\" },\n { id: \"C9\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No interstitial overlay in the first screen.\" },\n { id: \"C10\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every tel: href is E.164 (national digits for 13, 1300 and 1800) and matches the evidence-file phone.\" },\n { id: \"E1\", tier: \"shell\", from: \"3.8.0\", kind: \"static\", summary: \"Title patterns on new or changed URLs; recorded titles unchanged.\" },\n { id: \"E2\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Exactly one H1 on every page, rendered once.\" },\n { id: \"E8\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No banned phrase from any of the three layers, environmental claims included.\" },\n { id: \"T3\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"A licence line renders wherever licensed work is advertised, with the licensee where it differs.\" },\n { id: \"T4\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"No hard-coded rating or review numbers; proof claims resolve to renderable evidence.\" },\n { id: \"T11\", tier: \"shell\", from: \"3.8.0\", kind: \"static\", summary: \"No page for work over $5,000 outside the licence classes held; licence conditions carried.\" },\n { id: \"X1\", tier: \"shell\", from: \"3.8.0\", kind: \"record\", summary: \"Legacy URLs answer 200 or one redirect hop; earning-URL depth, titles and dollar figures kept.\" },\n { id: \"B1\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: \"Lab budgets: JavaScript, third-party requests, hero image and HTML within budget; no unrecorded chat widget, pixel, heatmap or tag manager.\" },\n { id: \"L10-hero\", tier: \"shell\", from: \"3.8.0\", kind: \"rendered\", summary: 'No kilometre figures, \"km from\" or \"based in\" in hero copy.' },\n { id: \"W2\", tier: \"shell\", from: \"3.13.0\", kind: \"journey\", summary: \"Prefetch by link role at 390 x 844 and 1440 x 900: at most 8 pages on landing and 12 after a full scroll; none from footer, area, nearby, map or finder links, hidden links or the current page; prefetch bytes above twice the page's HTML warn.\" },\n { id: \"W5\", tier: \"shell\", from: \"3.13.0\", kind: \"static\", summary: \"vercel.json regions equal hosting.originRegion (syd1 by default); any other region has an origin-region-v1 decision whose reviewBy has not passed.\" },\n { id: \"W10\", tier: \"shell\", from: \"3.13.0\", kind: \"rendered\", summary: \"robots.txt and the recorded firewall rules leave every search and answer crawler open; no Crawl-delay for them.\" },\n\n // enquiry: the form, thank-you page, reviews, proof strip, footer, CTA band, contact page, prices, media, density\n { id: \"C1\", tier: \"enquiry\", from: \"3.8.0\", kind: \"journey\", summary: \"Money pages: the form in the first section; at 390 x 844 the H1, a call and a quote action in the first viewport; at 1440 x 900 the form.\" },\n { id: \"C2\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: 'Form layout and labels; phone and email required; no helper or reassurance lines, no \"(required)\", no job select.' },\n { id: \"E9\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Every cost guide carries at least 8 dollar figures in a renderable price state.\" },\n { id: \"T5\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Reviews from the feed, with Google's rating, count and link beside any selection; none on the contact page; no empty reviews section.\" },\n { id: \"K8\", tier: \"enquiry\", from: \"3.8.0\", kind: \"rendered\", summary: \"Section contract: no empty section, one H2 each, at most one band tone, neighbours differ, proof within the first five sections.\" },\n { id: \"C7\", tier: \"enquiry\", from: \"3.10.0\", kind: \"rendered\", summary: \"Thank-you page: a reply time, a tel: link and noindex, follow.\" },\n { id: \"C8\", tier: \"enquiry\", from: \"3.10.0\", kind: \"rendered\", summary: \"Forgiving form: tolerant phone input (13, 1300 and 1800 too), autocomplete tokens, no capture on the photo input.\" },\n { id: \"C11\", tier: \"enquiry\", from: \"3.10.0\", kind: \"static\", summary: \"The test marker is stripped before delivery and reporting; /api/contact is rate-limited.\" },\n { id: \"N9\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: 'Footer content: column 1 brand, licences, socials and contact; services, areas and company columns; © name, ABN and legal links; no CTA, \"based in\", hours or guides column.' },\n { id: \"K2\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"Every rendered shared component has a skin entry; no reference-skin class ships.\" },\n { id: \"K11\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"Density: no section with more than one H2, more than 8 cards or more than 2 calls to action; measure and section padding within limits.\" },\n { id: \"K12-contact\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"The contact page is the page header, then the contact block, and nothing else.\" },\n { id: \"T6\", tier: \"enquiry\", from: \"3.11.0\", kind: \"static\", summary: \"Every image has a media.ts entry with provenance.\" },\n { id: \"T7\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: \"No image is the main image of two pages.\" },\n { id: \"T8\", tier: \"enquiry\", from: \"3.11.0\", kind: \"rendered\", summary: 'Prices: GST flag set; no \"+ GST\" or \"from $X\" teasers on cards and menus; one band per job site-wide.' },\n { id: \"T9\", tier: \"enquiry\", from: \"3.11.0\", kind: \"static\", summary: \"No image in /public carries location metadata.\" },\n\n // full: page families, location estates, section manifests, links and the data contract\n { id: \"K12\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"Section manifest: every required section rendered or skipped with a data-backed reason; conditional sections render when their data exists; canonical or recorded order.\" },\n { id: \"E5\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No orphan indexable pages, from the route catalogue and link data.\" },\n { id: \"E10\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Phrase-owner uniqueness in strategy data; no service-in-suburb page for the suburb page phrase; no service-region hub for the main trade.\" },\n { id: \"L3\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Neighbours by real distance.\" },\n { id: \"L4\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"No copy rotated by page address, no name-swapped FAQs, no other suburb's job without the nearby label.\" },\n { id: \"L6\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Location pages have 6 or more inbound links from 3 or more page types.\" },\n { id: \"L7\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"Location pages are 3 clicks or fewer from home.\" },\n { id: \"L8\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"A hub per family per region.\" },\n { id: \"L9\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: \"Data contract on service-in-suburb pages: typical prices or the cost-guide link, reviews or their fallback, the quote path (form with the suburb prefilled, phone, call bar); the nearby job card or none.\" },\n { id: \"L10\", tier: \"full\", from: \"3.12.0\", kind: \"rendered\", summary: 'No kilometre figures, \"km from\", \"based in\" or year stamps as local detail in location copy.' },\n { id: \"S4\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No hand-written per-service or per-suburb files; no dynamic route with zero pages.\" },\n { id: \"W9\", tier: \"full\", from: \"3.13.0\", kind: \"journey\", summary: \"URL variants: an unknown slug in each family answers the shared 404, the uppercase slug the same 404 or one permanent redirect to its lowercase form; a tracking query serves the same cached page as the clean URL.\" },\n { id: \"W9-params\", tier: \"full\", from: \"3.13.0\", kind: \"static\", summary: \"Pages under a dynamic route never read searchParams, so a query string cannot make an estate page dynamic.\" },\n { id: \"LOC-INDEX\", tier: \"full\", from: \"3.12.0\", kind: \"static\", summary: \"No location route is noindex or left out of the sitemap unless the recorded wave schedule dates it (Romie's 20 Oct decision: every suburb page stays indexed).\" },\n { id: \"W3\", tier: \"full\", from: \"3.13.0\", kind: \"static\", summary: \"No estate family (suburb, service-in-suburb, region, council) refreshes more often than every 7 days unless siteSystem.refreshExceptions records the reason and a decision id.\" },\n\n // new builds and redesigns\n { id: \"D4\", tier: \"new-build\", from: \"3.8.0\", kind: \"static\", summary: \"Same-trade distinctness: at least four of seven look levers differ from every listed competitor (advisory in 3.8.0, blocking from 3.9.0).\" },\n] as const satisfies readonly SiteSystemCheck[];\n\nexport type SiteSystemCheckId = (typeof SITE_SYSTEM_CHECKS)[number][\"id\"];\n\nconst TIER_RANK: Record<SiteSystemTier, number> = { shell: 0, enquiry: 1, full: 2 };\n\n/** A release as one comparable number: \"3.8.0\" < \"3.10.0\". */\nexport function siteSystemReleaseRank(version: string): number {\n const [major = 0, minor = 0, patch = 0] = version.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);\n return major * 1_000_000 + minor * 1_000 + patch;\n}\n\n/**\n * Whether a check blocks for a site at `tier` on `release`. Checks of higher tiers, and checks newer than the\n * installed release, report without blocking. D4 blocks new builds and redesigns from 3.9.0 whatever their tier.\n */\nexport function siteSystemCheckBlocks(\n check: Pick<SiteSystemCheck, \"id\" | \"tier\" | \"from\">,\n site: { tier: SiteSystemTier; release: string; newBuild?: boolean },\n): boolean {\n if (check.tier === \"new-build\") return site.newBuild === true && siteSystemReleaseRank(site.release) >= siteSystemReleaseRank(\"3.9.0\");\n if (siteSystemReleaseRank(site.release) < siteSystemReleaseRank(check.from)) return false;\n return TIER_RANK[check.tier] <= TIER_RANK[site.tier];\n}\n\n/** One result of one check, from any runner. `brandlab-ready` merges them into the ready report. */\nexport interface SiteSystemFinding {\n readonly checkId: SiteSystemCheckId;\n readonly status: \"pass\" | \"fail\" | \"not-applicable\";\n /** The page path or file the result is about, when it is about one. */\n readonly target?: string;\n /** One plain sentence: what passed or what is wrong and where. */\n readonly message: string;\n /** A warning never blocks: the check still passes and the report shows \"pass (warning)\". */\n readonly severity?: \"warning\";\n}\n"]}
|
|
@@ -13,6 +13,17 @@ export interface SiteSystemSettings {
|
|
|
13
13
|
readonly thirdParties: readonly string[];
|
|
14
14
|
/** Extra tel: links the site legitimately shows (C10). */
|
|
15
15
|
readonly phones: readonly string[];
|
|
16
|
+
/** Estate families allowed to refresh more often than every 7 days, with the reason and decision (W3, 3.13.0); absent when none. */
|
|
17
|
+
readonly refreshExceptions?: readonly SiteSystemRefreshException[];
|
|
18
|
+
}
|
|
19
|
+
/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */
|
|
20
|
+
export declare const SITE_SYSTEM_ESTATE_FAMILIES: readonly ["suburb", "service-in-suburb", "region", "council"];
|
|
21
|
+
/** A recorded W3 exception: `family` may refresh as often as every `revalidateSeconds`. */
|
|
22
|
+
export interface SiteSystemRefreshException {
|
|
23
|
+
readonly family: (typeof SITE_SYSTEM_ESTATE_FAMILIES)[number];
|
|
24
|
+
readonly revalidateSeconds: number;
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
readonly decisionId: string;
|
|
16
27
|
}
|
|
17
28
|
/** A Romie-owned holdout; `ruleId` names the check it covers. */
|
|
18
29
|
export interface SiteSystemException {
|
|
@@ -29,12 +40,28 @@ export interface SiteSystemProjectConfig {
|
|
|
29
40
|
readonly exceptions: readonly SiteSystemException[];
|
|
30
41
|
/** `searchCertification.representativeRoutes` (home, contact, service, location, blog). */
|
|
31
42
|
readonly representativeRoutes: Readonly<Record<string, string>>;
|
|
43
|
+
/** `hosting` (3.13.0): absent, written literally (its value) or computed (unreadable). Optional for older callers. */
|
|
44
|
+
readonly hosting?: SiteSystemHostingRecord;
|
|
32
45
|
/** False when defineBrandlabProject({ ... }) spreads or computes a member, so a tier could be hidden. */
|
|
33
46
|
readonly complete: boolean;
|
|
34
47
|
/** Fields that could not be read; the gate reports them and carries on without them. */
|
|
35
48
|
readonly problems: readonly string[];
|
|
36
49
|
}
|
|
50
|
+
/** What the gate read of `hosting` in brandlab.config.ts. */
|
|
51
|
+
export type SiteSystemHostingRecord = {
|
|
52
|
+
readonly state: "absent";
|
|
53
|
+
} | {
|
|
54
|
+
readonly state: "literal";
|
|
55
|
+
readonly value: unknown;
|
|
56
|
+
} | {
|
|
57
|
+
readonly state: "computed";
|
|
58
|
+
};
|
|
37
59
|
export declare function readSiteSystemProjectConfig(root: string): SiteSystemProjectConfig;
|
|
38
60
|
/** Throws when siteSystem is present but unreadable or invalid: a declared tier must never be skipped silently. */
|
|
39
61
|
export declare function parseSiteSystemProjectConfig(text: string): SiteSystemProjectConfig;
|
|
62
|
+
/**
|
|
63
|
+
* `hosting` from brandlab.config.ts text, without throwing: absent when there is no literal defineBrandlabProject
|
|
64
|
+
* call or no hosting member. Engineering checks and the ready gate (W5, W10) read it this way.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseProjectHosting(text: string): SiteSystemHostingRecord;
|
|
40
67
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"AAQA,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"AAQA,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;CACpE;AAED,0FAA0F;AAC1F,eAAO,MAAM,2BAA2B,+DAAgE,CAAC;AAEzG,2FAA2F;AAC3F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9D,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACpD,2FAA2F;IAC3F,QAAQ,CAAC,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChE,sHAAsH;IACtH,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC3C,yGAAyG;IACzG,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,6DAA6D;AAC7D,MAAM,MAAM,uBAAuB,GAC/B;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAAC;AAQnC,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAIjF;AAED,mHAAmH;AACnH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAoDlF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAIzE"}
|
|
@@ -7,8 +7,10 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import ts from "typescript";
|
|
9
9
|
import { SITE_SYSTEM_TIERS } from "./checks.js";
|
|
10
|
+
/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */
|
|
11
|
+
export const SITE_SYSTEM_ESTATE_FAMILIES = ["suburb", "service-in-suburb", "region", "council"];
|
|
10
12
|
const CONFIG_FILE = "brandlab.config.ts";
|
|
11
|
-
const SITE_SYSTEM_KEYS = new Set(["tier", "newBuild", "samples", "labels", "freeQuotes", "thirdParties", "phones"]);
|
|
13
|
+
const SITE_SYSTEM_KEYS = new Set(["tier", "newBuild", "samples", "labels", "freeQuotes", "thirdParties", "phones", "refreshExceptions"]);
|
|
12
14
|
const HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
13
15
|
const TEL_HREF_PATTERN = /^tel:\+?\d{6,15}$/;
|
|
14
16
|
const NOT_LITERAL = Symbol("not literal");
|
|
@@ -66,10 +68,27 @@ export function parseSiteSystemProjectConfig(text) {
|
|
|
66
68
|
siteSystem,
|
|
67
69
|
exceptions,
|
|
68
70
|
representativeRoutes,
|
|
71
|
+
hosting: hostingRecord(config),
|
|
69
72
|
complete,
|
|
70
73
|
problems,
|
|
71
74
|
};
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `hosting` from brandlab.config.ts text, without throwing: absent when there is no literal defineBrandlabProject
|
|
78
|
+
* call or no hosting member. Engineering checks and the ready gate (W5, W10) read it this way.
|
|
79
|
+
*/
|
|
80
|
+
export function parseProjectHosting(text) {
|
|
81
|
+
const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
82
|
+
const config = findProjectObject(source);
|
|
83
|
+
return config ? hostingRecord(config) : { state: "absent" };
|
|
84
|
+
}
|
|
85
|
+
function hostingRecord(config) {
|
|
86
|
+
const node = property(config, "hosting");
|
|
87
|
+
if (node === undefined)
|
|
88
|
+
return { state: "absent" };
|
|
89
|
+
const value = literal(node);
|
|
90
|
+
return value === NOT_LITERAL ? { state: "computed" } : { state: "literal", value };
|
|
91
|
+
}
|
|
73
92
|
/** `key: value` or `key` with an identifier or string key; spreads, computed keys and methods are not. */
|
|
74
93
|
function isPlainProperty(member) {
|
|
75
94
|
if (ts.isShorthandPropertyAssignment(member))
|
|
@@ -104,8 +123,35 @@ function readSiteSystem(value) {
|
|
|
104
123
|
freeQuotes: value.freeQuotes ?? null,
|
|
105
124
|
thirdParties: stringList(value.thirdParties, "thirdParties", "hostnames such as www.youtube.com", (host) => HOSTNAME_PATTERN.test(host)),
|
|
106
125
|
phones: stringList(value.phones, "phones", "tel: links such as tel:+61290001234", (href) => TEL_HREF_PATTERN.test(href)),
|
|
126
|
+
...(value.refreshExceptions !== undefined ? { refreshExceptions: refreshExceptions(value.refreshExceptions) } : {}),
|
|
107
127
|
};
|
|
108
128
|
}
|
|
129
|
+
/** The W3 exceptions, as defineBrandlabProject validates them; anything else is a configuration error. */
|
|
130
|
+
function refreshExceptions(value) {
|
|
131
|
+
const expected = `siteSystem.refreshExceptions in ${CONFIG_FILE} must be a literal list of { family, revalidateSeconds, reason, decisionId }`;
|
|
132
|
+
if (!Array.isArray(value))
|
|
133
|
+
throw new Error(`${expected}.`);
|
|
134
|
+
const families = new Set();
|
|
135
|
+
return value.map((entry, index) => {
|
|
136
|
+
if (!isRecord(entry) || Object.keys(entry).some((key) => !["family", "revalidateSeconds", "reason", "decisionId"].includes(key))) {
|
|
137
|
+
throw new Error(`${expected}; entry ${index} is not.`);
|
|
138
|
+
}
|
|
139
|
+
const { family, revalidateSeconds, reason, decisionId } = entry;
|
|
140
|
+
if (!SITE_SYSTEM_ESTATE_FAMILIES.includes(family) ||
|
|
141
|
+
!Number.isSafeInteger(revalidateSeconds) ||
|
|
142
|
+
revalidateSeconds < 60 ||
|
|
143
|
+
revalidateSeconds >= 604_800 ||
|
|
144
|
+
typeof reason !== "string" ||
|
|
145
|
+
!reason.trim() ||
|
|
146
|
+
typeof decisionId !== "string" ||
|
|
147
|
+
!decisionId.trim() ||
|
|
148
|
+
families.has(family)) {
|
|
149
|
+
throw new Error(`${expected}; entry ${index} needs one of ${SITE_SYSTEM_ESTATE_FAMILIES.join(", ")} (once each), whole seconds from 60 to 604799, a reason and a decision id.`);
|
|
150
|
+
}
|
|
151
|
+
families.add(family);
|
|
152
|
+
return { family: family, revalidateSeconds: revalidateSeconds, reason, decisionId };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
109
155
|
/** A literal list of unique strings that each pass `valid`, or [] when absent. */
|
|
110
156
|
function stringList(value, key, expected, valid) {
|
|
111
157
|
if (value === undefined)
|
|
@@ -181,6 +227,9 @@ function literal(node) {
|
|
|
181
227
|
return value.text;
|
|
182
228
|
if (ts.isNumericLiteral(value))
|
|
183
229
|
return Number(value.text);
|
|
230
|
+
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)) {
|
|
231
|
+
return -Number(value.operand.text);
|
|
232
|
+
}
|
|
184
233
|
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
185
234
|
return true;
|
|
186
235
|
if (value.kind === ts.SyntaxKind.FalseKeyword)
|