@brandlab-digital/conformance 3.14.0 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -3
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +18 -2
- package/dist/engineering.js.map +1 -1
- package/dist/estate-snapshot.d.ts +54 -0
- package/dist/estate-snapshot.d.ts.map +1 -0
- package/dist/estate-snapshot.js +164 -0
- package/dist/estate-snapshot.js.map +1 -0
- package/dist/estate.d.ts +10 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +36 -16
- package/dist/estate.js.map +1 -1
- package/dist/media-snapshot.d.ts +53 -0
- package/dist/media-snapshot.d.ts.map +1 -0
- package/dist/media-snapshot.js +66 -0
- package/dist/media-snapshot.js.map +1 -0
- package/dist/site-system/checks.d.ts +2 -2
- package/dist/site-system/checks.js +2 -2
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/config.d.ts +13 -0
- package/dist/site-system/config.d.ts.map +1 -1
- package/dist/site-system/config.js +30 -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/conversion.d.ts.map +1 -1
- package/dist/site-system/journeys/conversion.js +15 -2
- package/dist/site-system/journeys/conversion.js.map +1 -1
- package/dist/site-system/journeys/menu.d.ts.map +1 -1
- package/dist/site-system/journeys/menu.js +24 -1
- package/dist/site-system/journeys/menu.js.map +1 -1
- package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
- package/dist/site-system/journeys/page-helpers.js +1 -1
- package/dist/site-system/journeys/page-helpers.js.map +1 -1
- package/dist/site-system/journeys/plan.d.ts +13 -3
- package/dist/site-system/journeys/plan.d.ts.map +1 -1
- package/dist/site-system/journeys/plan.js +12 -2
- package/dist/site-system/journeys/plan.js.map +1 -1
- package/dist/site-system/journeys/shell.d.ts +1 -1
- package/dist/site-system/journeys/shell.js +2 -2
- package/dist/site-system/journeys/shell.js.map +1 -1
- package/dist/site-system/report.d.ts.map +1 -1
- package/dist/site-system/report.js +24 -4
- package/dist/site-system/report.js.map +1 -1
- package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
- package/dist/site-system/rules/form/c7-thank-you.js +26 -3
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
- package/dist/site-system/rules/form/index.d.ts.map +1 -1
- package/dist/site-system/rules/form/index.js +11 -1
- package/dist/site-system/rules/form/index.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +2 -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 +10 -2
- package/dist/site-system/rules/full/estate.d.ts.map +1 -1
- package/dist/site-system/rules/full/estate.js +35 -5
- package/dist/site-system/rules/full/estate.js.map +1 -1
- package/dist/site-system/rules/full/geography.d.ts +7 -0
- package/dist/site-system/rules/full/geography.d.ts.map +1 -1
- package/dist/site-system/rules/full/geography.js +9 -0
- package/dist/site-system/rules/full/geography.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 +5 -1
- package/dist/site-system/rules/full/index.js.map +1 -1
- package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
- package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l10-location-copy.js +5 -2
- package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
- package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.js +4 -1
- package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.js +8 -3
- package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
- package/dist/site-system/rules/full/l9-data-contract.js +2 -2
- package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
- package/dist/site-system/rules/full/links.d.ts +3 -1
- package/dist/site-system/rules/full/links.d.ts.map +1 -1
- package/dist/site-system/rules/full/links.js +15 -5
- package/dist/site-system/rules/full/links.js.map +1 -1
- package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
- package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
- package/dist/site-system/rules/full/route-kinds.js +52 -0
- package/dist/site-system/rules/full/route-kinds.js.map +1 -0
- package/dist/site-system/rules/full/strategy.d.ts +2 -0
- package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
- package/dist/site-system/rules/full/strategy.js +86 -14
- package/dist/site-system/rules/full/strategy.js.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.js +9 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
- package/dist/site-system-e2e.d.ts +1 -1
- package/dist/site-system-e2e.d.ts.map +1 -1
- package/dist/site-system-e2e.js +17 -4
- package/dist/site-system-e2e.js.map +1 -1
- package/package.json +10 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-snapshot.js","sourceRoot":"","sources":["../src/media-snapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The media manifest snapshot (3.15.0): a site whose `src/data/media.ts` is computed (gallery entries spread in from\n * its gallery data, for example) records the manifest as plain JSON, so the ready gate's T6 can read every entry\n * without running the site's code. Like the estate plan snapshot it is deterministic (entries by id, keys sorted, no\n * timestamp) and carries a hash of the files it was written from (`src/data` by default), so T6 fails a stale one.\n *\n * // tests/data/mediaSnapshot.test.ts\n * import { writeMediaSnapshot } from '@brandlab-digital/conformance/media-snapshot'\n * import { media } from '@/data/media'\n *\n * it('records the media manifest for the ready gate', () => {\n * const { changed } = writeMediaSnapshot(media, { projectRoot: process.cwd() })\n * if (process.env.CI) expect(changed).toBe(false)\n * })\n *\n * Node only: it reads and writes files.\n */\nimport { estatePlanSourceHash, snapshotSourcePaths, writeSnapshotFile, type EstatePlanSnapshotOptions } from \"./estate-snapshot.js\";\n\n/** Where the snapshot lives, relative to the project root. */\nexport const MEDIA_SNAPSHOT_FILE = \"src/data/media.generated.json\";\nexport const MEDIA_SNAPSHOT_KIND = \"brandlab-media-snapshot\";\nexport const MEDIA_SNAPSHOT_VERSION = 1;\n\ntype JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\n\n/** One manifest entry as `defineMedia` holds it: `src` and the rest of its fields. */\nexport interface MediaSnapshotEntry {\n readonly src: string;\n readonly [field: string]: unknown;\n}\n\nexport interface MediaSnapshot {\n readonly kind: typeof MEDIA_SNAPSHOT_KIND;\n readonly version: typeof MEDIA_SNAPSHOT_VERSION;\n readonly sources: { readonly paths: readonly string[]; readonly sha256: string };\n /** Entries by id, in id order, each with its keys sorted. */\n readonly media: Readonly<Record<string, Readonly<Record<string, JsonValue>>>>;\n}\n\nfunction byKey<T>(entries: [string, T][]): [string, T][] {\n return entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));\n}\n\n/** Plain JSON with sorted keys; values JSON cannot hold (undefined, functions) are left out. */\nfunction canonical(value: unknown): JsonValue | undefined {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : undefined;\n if (Array.isArray(value)) return value.map((item) => canonical(item) ?? null);\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>).flatMap(([key, item]): [string, JsonValue][] => {\n const plain = canonical(item);\n return plain === undefined ? [] : [[key, plain]];\n });\n return Object.fromEntries(byKey(entries));\n }\n return undefined;\n}\n\n/** The snapshot for a media manifest: every entry in canonical form, with the sources' hash. */\nexport function mediaSnapshot(media: Readonly<Record<string, MediaSnapshotEntry>>, options: EstatePlanSnapshotOptions): MediaSnapshot {\n const paths = snapshotSourcePaths(options.sources);\n const entries = byKey(\n Object.entries(media).map(([id, entry]): [string, Record<string, JsonValue>] => {\n if (!entry || typeof entry.src !== \"string\" || entry.src === \"\") throw new Error(`media.${id} has no src.`);\n return [id, canonical(entry) as Record<string, JsonValue>];\n }),\n );\n return {\n kind: MEDIA_SNAPSHOT_KIND,\n version: MEDIA_SNAPSHOT_VERSION,\n sources: { paths, sha256: estatePlanSourceHash(options.projectRoot, paths) },\n media: Object.fromEntries(entries),\n };\n}\n\n/**\n * Writes `src/data/media.generated.json` when its content differs; `changed` says whether it did. Run it from the\n * site's tests so the snapshot follows media.ts, and commit the file.\n */\nexport function writeMediaSnapshot(\n media: Readonly<Record<string, MediaSnapshotEntry>>,\n options: EstatePlanSnapshotOptions,\n): { readonly path: string; readonly changed: boolean; readonly snapshot: MediaSnapshot } {\n const snapshot = mediaSnapshot(media, options);\n return { ...writeSnapshotFile(options.projectRoot, MEDIA_SNAPSHOT_FILE, `${JSON.stringify(snapshot, null, 2)}\\n`), snapshot };\n}\n"]}
|
|
@@ -66,7 +66,7 @@ export declare const SITE_SYSTEM_CHECKS: readonly [{
|
|
|
66
66
|
readonly tier: "shell";
|
|
67
67
|
readonly from: "3.8.0";
|
|
68
68
|
readonly kind: "journey";
|
|
69
|
-
readonly summary: "The header stays at the top with the phone and quote visible at every desktop width; the compact band
|
|
69
|
+
readonly summary: "The header stays at the top with the phone and quote visible at every desktop width; the compact band (1024 to 1199 px) shows them too.";
|
|
70
70
|
}, {
|
|
71
71
|
readonly id: "N8";
|
|
72
72
|
readonly tier: "shell";
|
|
@@ -120,7 +120,7 @@ export declare const SITE_SYSTEM_CHECKS: readonly [{
|
|
|
120
120
|
readonly tier: "shell";
|
|
121
121
|
readonly from: "3.8.0";
|
|
122
122
|
readonly kind: "journey";
|
|
123
|
-
readonly summary: "Phone taps report an allowed placement (header, footer, hero, cta, fab, contact, location, page).";
|
|
123
|
+
readonly summary: "Phone taps report an allowed placement (header, footer, hero, cta, fab, contact, location, page, emergency).";
|
|
124
124
|
}, {
|
|
125
125
|
readonly id: "C5";
|
|
126
126
|
readonly tier: "shell";
|
|
@@ -21,7 +21,7 @@ export const SITE_SYSTEM_CHECKS = [
|
|
|
21
21
|
{ 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." },
|
|
22
22
|
{ 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." },
|
|
23
23
|
{ 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." },
|
|
24
|
-
{ 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
|
|
24
|
+
{ 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 (1024 to 1199 px) shows them too." },
|
|
25
25
|
{ 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)." },
|
|
26
26
|
{ 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." },
|
|
27
27
|
{ id: "K3", tier: "shell", from: "3.8.0", kind: "journey", summary: "Text contrast 4.5:1 and non-text 3:1 in the browser." },
|
|
@@ -30,7 +30,7 @@ export const SITE_SYSTEM_CHECKS = [
|
|
|
30
30
|
{ 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." },
|
|
31
31
|
{ 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." },
|
|
32
32
|
{ 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." },
|
|
33
|
-
{ 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)." },
|
|
33
|
+
{ 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, emergency)." },
|
|
34
34
|
{ id: "C5", tier: "shell", from: "3.8.0", kind: "rendered", summary: "Call-to-action labels come from the closed list or a recorded label." },
|
|
35
35
|
{ id: "C9", tier: "shell", from: "3.8.0", kind: "rendered", summary: "No interstitial overlay in the first screen." },
|
|
36
36
|
{ 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." },
|
|
@@ -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 * `launch`: a not-run result blocks only under `brandlab-ready --launch`, where it counts as a failure. For checks\n * that need production values in the shell (H13), so a CI run reports them as not run without failing the tier.\n */\n readonly notRunBlocks?: \"launch\";\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 { id: \"H13\", tier: \"enquiry\", from: \"3.14.0\", kind: \"static\", notRunBlocks: \"launch\", summary: \"The business inbox can receive enquiries: a real sender (no .invalid, example or sandbox domain), MX records for the inbox, the sender domain verified in Resend and the inbox not on Resend's suppression list (not run without the inbox variable and a full-access Resend key in the shell, which blocks only at launch).\" },\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: \"K12-slots\", tier: \"full\", from: \"3.14.0\", kind: \"rendered\", summary: \"Trade lines inside sections (matrix part B): for trees, the council permit note in LocalFacts on suburb and service-in-suburb pages and the qualifications and insurance lines on the about page, or a data-backed reason they are hidden.\" },\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: \"E1-pattern\", tier: \"full\", from: \"3.14.0\", kind: \"rendered\", summary: 'Location pages outside the recorded titles have the H1 \"{Service} {Place}\" (no \"in\", no \"the\") and a title that starts with it, at most 60 characters.' },\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 /** `not-run`: the rule could not judge the check here (a missing key or input); it names what to supply. */\n readonly status: \"pass\" | \"fail\" | \"not-run\" | \"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 * `launch`: a not-run result blocks only under `brandlab-ready --launch`, where it counts as a failure. For checks\n * that need production values in the shell (H13), so a CI run reports them as not run without failing the tier.\n */\n readonly notRunBlocks?: \"launch\";\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 (1024 to 1199 px) shows them too.\" },\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, emergency).\" },\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 { id: \"H13\", tier: \"enquiry\", from: \"3.14.0\", kind: \"static\", notRunBlocks: \"launch\", summary: \"The business inbox can receive enquiries: a real sender (no .invalid, example or sandbox domain), MX records for the inbox, the sender domain verified in Resend and the inbox not on Resend's suppression list (not run without the inbox variable and a full-access Resend key in the shell, which blocks only at launch).\" },\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: \"K12-slots\", tier: \"full\", from: \"3.14.0\", kind: \"rendered\", summary: \"Trade lines inside sections (matrix part B): for trees, the council permit note in LocalFacts on suburb and service-in-suburb pages and the qualifications and insurance lines on the about page, or a data-backed reason they are hidden.\" },\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: \"E1-pattern\", tier: \"full\", from: \"3.14.0\", kind: \"rendered\", summary: 'Location pages outside the recorded titles have the H1 \"{Service} {Place}\" (no \"in\", no \"the\") and a title that starts with it, at most 60 characters.' },\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 /** `not-run`: the rule could not judge the check here (a missing key or input); it names what to supply. */\n readonly status: \"pass\" | \"fail\" | \"not-run\" | \"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"]}
|
|
@@ -15,7 +15,20 @@ export interface SiteSystemSettings {
|
|
|
15
15
|
readonly phones: readonly string[];
|
|
16
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
17
|
readonly refreshExceptions?: readonly SiteSystemRefreshException[];
|
|
18
|
+
/** What the thank-you page promises about a reply (C7, 3.15.0); absent when not recorded (a specific time is required). */
|
|
19
|
+
readonly replyTime?: SiteSystemReplyTime;
|
|
18
20
|
}
|
|
21
|
+
/** site-core `BrandlabReplyTimeRecord`: a specific reply time, or a recorded decision to promise none and the wording used instead. */
|
|
22
|
+
export type SiteSystemReplyTime = {
|
|
23
|
+
readonly promise: true;
|
|
24
|
+
} | {
|
|
25
|
+
readonly promise: false;
|
|
26
|
+
readonly wording: string;
|
|
27
|
+
readonly decidedBy: "owner" | "romie";
|
|
28
|
+
readonly decidedOn: string;
|
|
29
|
+
};
|
|
30
|
+
/** Wording that still promises a time (site-core `BRANDLAB_REPLY_TIME_PROMISE`): a no-promise record's wording must not match. */
|
|
31
|
+
export declare const SITE_SYSTEM_REPLY_TIME_PROMISE: RegExp;
|
|
19
32
|
/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */
|
|
20
33
|
export declare const SITE_SYSTEM_ESTATE_FAMILIES: readonly ["suburb", "service-in-suburb", "region", "council"];
|
|
21
34
|
/** A recorded W3 exception: `family` may refresh as often as every `revalidateSeconds`. */
|
|
@@ -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;IACnC,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,10 +7,12 @@ 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
|
+
/** Wording that still promises a time (site-core `BRANDLAB_REPLY_TIME_PROMISE`): a no-promise record's wording must not match. */
|
|
11
|
+
export const SITE_SYSTEM_REPLY_TIME_PROMISE = /\d|\b(?:minutes?|mins?|hours?|hrs?|today|tomorrow|overnight)\b|\b(?:same|next)[-\s]+(?:business[-\s]+|working[-\s]+)?day\b|\b(?:business|working)[-\s]+days?\b|\bend\s+of\s+(?:the\s+)?(?:business\s+)?day\b/i;
|
|
10
12
|
/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */
|
|
11
13
|
export const SITE_SYSTEM_ESTATE_FAMILIES = ["suburb", "service-in-suburb", "region", "council"];
|
|
12
14
|
const CONFIG_FILE = "brandlab.config.ts";
|
|
13
|
-
const SITE_SYSTEM_KEYS = new Set(["tier", "newBuild", "samples", "labels", "freeQuotes", "thirdParties", "phones", "refreshExceptions"]);
|
|
15
|
+
const SITE_SYSTEM_KEYS = new Set(["tier", "newBuild", "samples", "labels", "freeQuotes", "thirdParties", "phones", "refreshExceptions", "replyTime"]);
|
|
14
16
|
const HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
15
17
|
const TEL_HREF_PATTERN = /^tel:\+?\d{6,15}$/;
|
|
16
18
|
const NOT_LITERAL = Symbol("not literal");
|
|
@@ -124,8 +126,35 @@ function readSiteSystem(value) {
|
|
|
124
126
|
thirdParties: stringList(value.thirdParties, "thirdParties", "hostnames such as www.youtube.com", (host) => HOSTNAME_PATTERN.test(host)),
|
|
125
127
|
phones: stringList(value.phones, "phones", "tel: links such as tel:+61290001234", (href) => TEL_HREF_PATTERN.test(href)),
|
|
126
128
|
...(value.refreshExceptions !== undefined ? { refreshExceptions: refreshExceptions(value.refreshExceptions) } : {}),
|
|
129
|
+
...(value.replyTime !== undefined ? { replyTime: replyTime(value.replyTime) } : {}),
|
|
127
130
|
};
|
|
128
131
|
}
|
|
132
|
+
/** The C7 reply-time record, as defineBrandlabProject validates it; anything else is a configuration error. */
|
|
133
|
+
function replyTime(value) {
|
|
134
|
+
const expected = `siteSystem.replyTime in ${CONFIG_FILE} must be { promise: true } or { promise: false, wording, decidedBy: "owner" | "romie", decidedOn: "YYYY-MM-DD" }, with wording that promises no time`;
|
|
135
|
+
if (!isRecord(value))
|
|
136
|
+
throw new Error(`${expected}.`);
|
|
137
|
+
if (value.promise === true && Object.keys(value).length === 1)
|
|
138
|
+
return { promise: true };
|
|
139
|
+
const { promise, wording, decidedBy, decidedOn } = value;
|
|
140
|
+
if (promise !== false ||
|
|
141
|
+
Object.keys(value).some((key) => !["promise", "wording", "decidedBy", "decidedOn"].includes(key)) ||
|
|
142
|
+
typeof wording !== "string" ||
|
|
143
|
+
!wording.trim() ||
|
|
144
|
+
SITE_SYSTEM_REPLY_TIME_PROMISE.test(wording) ||
|
|
145
|
+
(decidedBy !== "owner" && decidedBy !== "romie") ||
|
|
146
|
+
!isIsoDay(decidedOn)) {
|
|
147
|
+
throw new Error(`${expected}.`);
|
|
148
|
+
}
|
|
149
|
+
return { promise: false, wording, decidedBy, decidedOn: decidedOn };
|
|
150
|
+
}
|
|
151
|
+
/** A real YYYY-MM-DD calendar day. */
|
|
152
|
+
function isIsoDay(value) {
|
|
153
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
154
|
+
return false;
|
|
155
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
156
|
+
return new Date(Date.UTC(year, month - 1, day)).toISOString().slice(0, 10) === value;
|
|
157
|
+
}
|
|
129
158
|
/** The W3 exceptions, as defineBrandlabProject validates them; anything else is a configuration error. */
|
|
130
159
|
function refreshExceptions(value) {
|
|
131
160
|
const expected = `siteSystem.refreshExceptions in ${CONFIG_FILE} must be a literal list of { family, revalidateSeconds, reason, decisionId }`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the ready gate reads from brandlab.config.ts: the Site System tier, the exceptions record and the\n * representative routes. The file is parsed, never executed, so these fields must be written literally (as\n * `defineBrandlabProject` requires anyway).\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport ts from \"typescript\";\nimport { SITE_SYSTEM_TIERS, type SiteSystemTier } from \"./checks.js\";\n\nexport interface SiteSystemSettings {\n readonly tier: SiteSystemTier;\n /** A new build or redesign: D4 blocks whatever the tier. */\n readonly newBuild: boolean;\n /** Extra pages to sample for the rendered checks. */\n readonly samples: readonly string[];\n /** Quote and call-to-action labels recorded for this site, beyond the closed list (C5). */\n readonly labels: readonly string[];\n /** Whether the business offers free quotes; null when not recorded. */\n readonly freeQuotes: boolean | null;\n /** Third-party hostnames the site may load (B1). */\n readonly thirdParties: readonly string[];\n /** Extra tel: links the site legitimately shows (C10). */\n readonly phones: readonly string[];\n /** Estate families allowed to refresh more often than every 7 days, with the reason and decision (W3, 3.13.0); absent when none. */\n readonly refreshExceptions?: readonly SiteSystemRefreshException[];\n}\n\n/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */\nexport const SITE_SYSTEM_ESTATE_FAMILIES = [\"suburb\", \"service-in-suburb\", \"region\", \"council\"] as const;\n\n/** A recorded W3 exception: `family` may refresh as often as every `revalidateSeconds`. */\nexport interface SiteSystemRefreshException {\n readonly family: (typeof SITE_SYSTEM_ESTATE_FAMILIES)[number];\n readonly revalidateSeconds: number;\n readonly reason: string;\n readonly decisionId: string;\n}\n\n/** A Romie-owned holdout; `ruleId` names the check it covers. */\nexport interface SiteSystemException {\n readonly ruleId: string;\n readonly rationale: string;\n readonly evidence: string;\n readonly expiresAt?: string;\n readonly reviewTrigger: string;\n}\n\nexport interface SiteSystemProjectConfig {\n readonly projectId: string | null;\n /** Null when the site has not adopted Site System v2: nothing in the gate blocks. */\n readonly siteSystem: SiteSystemSettings | null;\n readonly exceptions: readonly SiteSystemException[];\n /** `searchCertification.representativeRoutes` (home, contact, service, location, blog). */\n readonly representativeRoutes: Readonly<Record<string, string>>;\n /** `hosting` (3.13.0): absent, written literally (its value) or computed (unreadable). Optional for older callers. */\n readonly hosting?: SiteSystemHostingRecord;\n /** False when defineBrandlabProject({ ... }) spreads or computes a member, so a tier could be hidden. */\n readonly complete: boolean;\n /** Fields that could not be read; the gate reports them and carries on without them. */\n readonly problems: readonly string[];\n}\n\n/** What the gate read of `hosting` in brandlab.config.ts. */\nexport type SiteSystemHostingRecord =\n | { readonly state: \"absent\" }\n | { readonly state: \"literal\"; readonly value: unknown }\n | { readonly state: \"computed\" };\n\nconst CONFIG_FILE = \"brandlab.config.ts\";\nconst SITE_SYSTEM_KEYS = new Set([\"tier\", \"newBuild\", \"samples\", \"labels\", \"freeQuotes\", \"thirdParties\", \"phones\", \"refreshExceptions\"]);\nconst HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/;\nconst TEL_HREF_PATTERN = /^tel:\\+?\\d{6,15}$/;\nconst NOT_LITERAL = Symbol(\"not literal\");\n\nexport function readSiteSystemProjectConfig(root: string): SiteSystemProjectConfig {\n const path = join(root, CONFIG_FILE);\n if (!existsSync(path)) throw new Error(`${CONFIG_FILE} is missing, so the ready gate cannot tell which tier applies.`);\n return parseSiteSystemProjectConfig(readFileSync(path, \"utf8\"));\n}\n\n/** Throws when siteSystem is present but unreadable or invalid: a declared tier must never be skipped silently. */\nexport function parseSiteSystemProjectConfig(text: string): SiteSystemProjectConfig {\n const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);\n const config = findProjectObject(source);\n if (!config) {\n throw new Error(`${CONFIG_FILE} has no literal defineBrandlabProject({ ... }) call.`);\n }\n const problems: string[] = [];\n const complete = config.properties.every(isPlainProperty);\n if (!complete) {\n problems.push(`defineBrandlabProject({ ... }) in ${CONFIG_FILE} spreads or computes a member, so the ready gate cannot read it in full; write every field literally.`);\n }\n\n const projectId = literal(property(config, \"projectId\"));\n\n const siteSystemNode = property(config, \"siteSystem\");\n const siteSystem = siteSystemNode === undefined ? null : readSiteSystem(literal(siteSystemNode));\n\n const exceptionsNode = property(config, \"exceptions\");\n let exceptions: SiteSystemException[] = [];\n if (exceptionsNode !== undefined) {\n const value = literal(exceptionsNode);\n if (!Array.isArray(value)) {\n problems.push(`The exceptions in ${CONFIG_FILE} are not written as a literal list, so none were applied.`);\n } else {\n exceptions = value.flatMap((entry, index) => {\n const exception = readException(entry);\n if (!exception) problems.push(`exceptions[${index}] in ${CONFIG_FILE} has no literal ruleId, rationale, evidence and reviewTrigger, so it was not applied.`);\n return exception ? [exception] : [];\n });\n }\n }\n\n const searchCertification = literal(property(config, \"searchCertification\"));\n const routes = isRecord(searchCertification) ? searchCertification.representativeRoutes : undefined;\n const representativeRoutes: Record<string, string> = {};\n if (isRecord(routes)) {\n for (const [name, path] of Object.entries(routes)) {\n if (typeof path === \"string\" && path.startsWith(\"/\")) representativeRoutes[name] = path;\n }\n } else if (property(config, \"searchCertification\") !== undefined) {\n problems.push(`searchCertification.representativeRoutes in ${CONFIG_FILE} is not literal, so only the home page was sampled from it.`);\n }\n\n return {\n projectId: typeof projectId === \"string\" ? projectId : null,\n siteSystem,\n exceptions,\n representativeRoutes,\n hosting: hostingRecord(config),\n complete,\n problems,\n };\n}\n\n/**\n * `hosting` from brandlab.config.ts text, without throwing: absent when there is no literal defineBrandlabProject\n * call or no hosting member. Engineering checks and the ready gate (W5, W10) read it this way.\n */\nexport function parseProjectHosting(text: string): SiteSystemHostingRecord {\n const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);\n const config = findProjectObject(source);\n return config ? hostingRecord(config) : { state: \"absent\" };\n}\n\nfunction hostingRecord(config: ts.ObjectLiteralExpression): SiteSystemHostingRecord {\n const node = property(config, \"hosting\");\n if (node === undefined) return { state: \"absent\" };\n const value = literal(node);\n return value === NOT_LITERAL ? { state: \"computed\" } : { state: \"literal\", value };\n}\n\n/** `key: value` or `key` with an identifier or string key; spreads, computed keys and methods are not. */\nfunction isPlainProperty(member: ts.ObjectLiteralElementLike): boolean {\n if (ts.isShorthandPropertyAssignment(member)) return !member.objectAssignmentInitializer;\n return ts.isPropertyAssignment(member) && (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name));\n}\n\nfunction readSiteSystem(value: unknown): SiteSystemSettings {\n if (value === NOT_LITERAL || !isRecord(value)) {\n throw new Error(`siteSystem in ${CONFIG_FILE} must be written as a literal object, for example siteSystem: { tier: 'shell' }.`);\n }\n const unknown = Object.keys(value).filter((key) => !SITE_SYSTEM_KEYS.has(key));\n if (unknown.length > 0) throw new Error(`siteSystem in ${CONFIG_FILE} contains unsupported keys: ${unknown.join(\", \")}.`);\n if (!SITE_SYSTEM_TIERS.includes(value.tier as SiteSystemTier)) {\n throw new Error(`siteSystem.tier in ${CONFIG_FILE} must be ${SITE_SYSTEM_TIERS.join(\", \")}.`);\n }\n if (value.newBuild !== undefined && typeof value.newBuild !== \"boolean\") {\n throw new Error(`siteSystem.newBuild in ${CONFIG_FILE} must be true or false.`);\n }\n if (\n value.samples !== undefined &&\n (!Array.isArray(value.samples) || value.samples.some((path) => typeof path !== \"string\" || !path.startsWith(\"/\")))\n ) {\n throw new Error(`siteSystem.samples in ${CONFIG_FILE} must be a literal list of root-relative paths.`);\n }\n if (value.freeQuotes !== undefined && typeof value.freeQuotes !== \"boolean\") {\n throw new Error(`siteSystem.freeQuotes in ${CONFIG_FILE} must be true or false.`);\n }\n return {\n tier: value.tier as SiteSystemTier,\n newBuild: value.newBuild === true,\n samples: (value.samples as string[] | undefined) ?? [],\n labels: stringList(value.labels, \"labels\", \"non-empty labels\", (label) => label.trim().length > 0),\n freeQuotes: (value.freeQuotes as boolean | undefined) ?? null,\n thirdParties: stringList(value.thirdParties, \"thirdParties\", \"hostnames such as www.youtube.com\", (host) => HOSTNAME_PATTERN.test(host)),\n phones: stringList(value.phones, \"phones\", \"tel: links such as tel:+61290001234\", (href) => TEL_HREF_PATTERN.test(href)),\n ...(value.refreshExceptions !== undefined ? { refreshExceptions: refreshExceptions(value.refreshExceptions) } : {}),\n };\n}\n\n/** The W3 exceptions, as defineBrandlabProject validates them; anything else is a configuration error. */\nfunction refreshExceptions(value: unknown): SiteSystemRefreshException[] {\n const expected = `siteSystem.refreshExceptions in ${CONFIG_FILE} must be a literal list of { family, revalidateSeconds, reason, decisionId }`;\n if (!Array.isArray(value)) throw new Error(`${expected}.`);\n const families = new Set<string>();\n return value.map((entry, index) => {\n if (!isRecord(entry) || Object.keys(entry).some((key) => ![\"family\", \"revalidateSeconds\", \"reason\", \"decisionId\"].includes(key))) {\n throw new Error(`${expected}; entry ${index} is not.`);\n }\n const { family, revalidateSeconds, reason, decisionId } = entry;\n if (\n !(SITE_SYSTEM_ESTATE_FAMILIES as readonly unknown[]).includes(family) ||\n !Number.isSafeInteger(revalidateSeconds) ||\n (revalidateSeconds as number) < 60 ||\n (revalidateSeconds as number) >= 604_800 ||\n typeof reason !== \"string\" ||\n !reason.trim() ||\n typeof decisionId !== \"string\" ||\n !decisionId.trim() ||\n families.has(family as string)\n ) {\n 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.`);\n }\n families.add(family as string);\n return { family: family as SiteSystemRefreshException[\"family\"], revalidateSeconds: revalidateSeconds as number, reason, decisionId };\n });\n}\n\n/** A literal list of unique strings that each pass `valid`, or [] when absent. */\nfunction stringList(value: unknown, key: string, expected: string, valid: (item: string) => boolean): string[] {\n if (value === undefined) return [];\n if (\n !Array.isArray(value) ||\n value.some((item) => typeof item !== \"string\" || !valid(item)) ||\n new Set(value).size !== value.length\n ) {\n throw new Error(`siteSystem.${key} in ${CONFIG_FILE} must be a literal list of unique ${expected}.`);\n }\n return value as string[];\n}\n\nfunction readException(value: unknown): SiteSystemException | null {\n if (!isRecord(value)) return null;\n const { ruleId, rationale, evidence, expiresAt, reviewTrigger } = value;\n if (\n typeof ruleId !== \"string\" || !ruleId.trim() ||\n typeof rationale !== \"string\" ||\n typeof evidence !== \"string\" ||\n typeof reviewTrigger !== \"string\" ||\n (expiresAt !== undefined && typeof expiresAt !== \"string\")\n ) {\n return null;\n }\n return { ruleId, rationale, evidence, reviewTrigger, ...(expiresAt !== undefined ? { expiresAt } : {}) };\n}\n\nfunction findProjectObject(source: ts.SourceFile): ts.ObjectLiteralExpression | undefined {\n let found: ts.ObjectLiteralExpression | undefined;\n const visit = (node: ts.Node) => {\n if (found) return;\n if (ts.isCallExpression(node)) {\n const callee = node.expression;\n const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : null;\n const argument = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;\n if (name === \"defineBrandlabProject\" && argument && ts.isObjectLiteralExpression(argument)) {\n found = argument;\n return;\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(source);\n return found;\n}\n\nfunction property(object: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined {\n for (const member of object.properties) {\n if (ts.isShorthandPropertyAssignment(member) && member.name.text === name) return member.name;\n if (!ts.isPropertyAssignment(member)) continue;\n const key = member.name;\n if ((ts.isIdentifier(key) || ts.isStringLiteralLike(key)) && key.text === name) return member.initializer;\n }\n return undefined;\n}\n\nfunction unwrap(node: ts.Expression): ts.Expression {\n let current = node;\n while (\n ts.isParenthesizedExpression(current) ||\n ts.isAsExpression(current) ||\n ts.isSatisfiesExpression(current) ||\n ts.isNonNullExpression(current) ||\n ts.isTypeAssertionExpression(current)\n ) {\n current = current.expression;\n }\n return current;\n}\n\n/** The JSON value of a literal expression, or NOT_LITERAL when any part is computed. */\nfunction literal(node: ts.Expression | undefined): unknown {\n if (!node) return undefined;\n const value = unwrap(node);\n if (ts.isStringLiteralLike(value)) return value.text;\n if (ts.isNumericLiteral(value)) return Number(value.text);\n if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)) {\n return -Number(value.operand.text);\n }\n if (value.kind === ts.SyntaxKind.TrueKeyword) return true;\n if (value.kind === ts.SyntaxKind.FalseKeyword) return false;\n if (value.kind === ts.SyntaxKind.NullKeyword) return null;\n if (ts.isArrayLiteralExpression(value)) {\n const items = value.elements.map((element) => (ts.isSpreadElement(element) ? NOT_LITERAL : literal(element)));\n return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;\n }\n if (ts.isObjectLiteralExpression(value)) {\n const object: Record<string, unknown> = {};\n for (const member of value.properties) {\n if (!ts.isPropertyAssignment(member)) return NOT_LITERAL;\n const key = member.name;\n if (!ts.isIdentifier(key) && !ts.isStringLiteralLike(key)) return NOT_LITERAL;\n const item = literal(member.initializer);\n if (item === NOT_LITERAL) return NOT_LITERAL;\n object[key.text] = item;\n }\n return object;\n }\n return NOT_LITERAL;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/site-system/config.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the ready gate reads from brandlab.config.ts: the Site System tier, the exceptions record and the\n * representative routes. The file is parsed, never executed, so these fields must be written literally (as\n * `defineBrandlabProject` requires anyway).\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport ts from \"typescript\";\nimport { SITE_SYSTEM_TIERS, type SiteSystemTier } from \"./checks.js\";\n\nexport interface SiteSystemSettings {\n readonly tier: SiteSystemTier;\n /** A new build or redesign: D4 blocks whatever the tier. */\n readonly newBuild: boolean;\n /** Extra pages to sample for the rendered checks. */\n readonly samples: readonly string[];\n /** Quote and call-to-action labels recorded for this site, beyond the closed list (C5). */\n readonly labels: readonly string[];\n /** Whether the business offers free quotes; null when not recorded. */\n readonly freeQuotes: boolean | null;\n /** Third-party hostnames the site may load (B1). */\n readonly thirdParties: readonly string[];\n /** Extra tel: links the site legitimately shows (C10). */\n readonly phones: readonly string[];\n /** Estate families allowed to refresh more often than every 7 days, with the reason and decision (W3, 3.13.0); absent when none. */\n readonly refreshExceptions?: readonly SiteSystemRefreshException[];\n /** What the thank-you page promises about a reply (C7, 3.15.0); absent when not recorded (a specific time is required). */\n readonly replyTime?: SiteSystemReplyTime;\n}\n\n/** site-core `BrandlabReplyTimeRecord`: a specific reply time, or a recorded decision to promise none and the wording used instead. */\nexport type SiteSystemReplyTime =\n | { readonly promise: true }\n | { readonly promise: false; readonly wording: string; readonly decidedBy: \"owner\" | \"romie\"; readonly decidedOn: string };\n\n/** Wording that still promises a time (site-core `BRANDLAB_REPLY_TIME_PROMISE`): a no-promise record's wording must not match. */\nexport const SITE_SYSTEM_REPLY_TIME_PROMISE =\n /\\d|\\b(?:minutes?|mins?|hours?|hrs?|today|tomorrow|overnight)\\b|\\b(?:same|next)[-\\s]+(?:business[-\\s]+|working[-\\s]+)?day\\b|\\b(?:business|working)[-\\s]+days?\\b|\\bend\\s+of\\s+(?:the\\s+)?(?:business\\s+)?day\\b/i;\n\n/** The estate families check W3 covers (site-core `BRANDLAB_ESTATE_REFRESH_FAMILIES`). */\nexport const SITE_SYSTEM_ESTATE_FAMILIES = [\"suburb\", \"service-in-suburb\", \"region\", \"council\"] as const;\n\n/** A recorded W3 exception: `family` may refresh as often as every `revalidateSeconds`. */\nexport interface SiteSystemRefreshException {\n readonly family: (typeof SITE_SYSTEM_ESTATE_FAMILIES)[number];\n readonly revalidateSeconds: number;\n readonly reason: string;\n readonly decisionId: string;\n}\n\n/** A Romie-owned holdout; `ruleId` names the check it covers. */\nexport interface SiteSystemException {\n readonly ruleId: string;\n readonly rationale: string;\n readonly evidence: string;\n readonly expiresAt?: string;\n readonly reviewTrigger: string;\n}\n\nexport interface SiteSystemProjectConfig {\n readonly projectId: string | null;\n /** Null when the site has not adopted Site System v2: nothing in the gate blocks. */\n readonly siteSystem: SiteSystemSettings | null;\n readonly exceptions: readonly SiteSystemException[];\n /** `searchCertification.representativeRoutes` (home, contact, service, location, blog). */\n readonly representativeRoutes: Readonly<Record<string, string>>;\n /** `hosting` (3.13.0): absent, written literally (its value) or computed (unreadable). Optional for older callers. */\n readonly hosting?: SiteSystemHostingRecord;\n /** False when defineBrandlabProject({ ... }) spreads or computes a member, so a tier could be hidden. */\n readonly complete: boolean;\n /** Fields that could not be read; the gate reports them and carries on without them. */\n readonly problems: readonly string[];\n}\n\n/** What the gate read of `hosting` in brandlab.config.ts. */\nexport type SiteSystemHostingRecord =\n | { readonly state: \"absent\" }\n | { readonly state: \"literal\"; readonly value: unknown }\n | { readonly state: \"computed\" };\n\nconst CONFIG_FILE = \"brandlab.config.ts\";\nconst SITE_SYSTEM_KEYS = new Set([\"tier\", \"newBuild\", \"samples\", \"labels\", \"freeQuotes\", \"thirdParties\", \"phones\", \"refreshExceptions\", \"replyTime\"]);\nconst HOSTNAME_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/;\nconst TEL_HREF_PATTERN = /^tel:\\+?\\d{6,15}$/;\nconst NOT_LITERAL = Symbol(\"not literal\");\n\nexport function readSiteSystemProjectConfig(root: string): SiteSystemProjectConfig {\n const path = join(root, CONFIG_FILE);\n if (!existsSync(path)) throw new Error(`${CONFIG_FILE} is missing, so the ready gate cannot tell which tier applies.`);\n return parseSiteSystemProjectConfig(readFileSync(path, \"utf8\"));\n}\n\n/** Throws when siteSystem is present but unreadable or invalid: a declared tier must never be skipped silently. */\nexport function parseSiteSystemProjectConfig(text: string): SiteSystemProjectConfig {\n const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);\n const config = findProjectObject(source);\n if (!config) {\n throw new Error(`${CONFIG_FILE} has no literal defineBrandlabProject({ ... }) call.`);\n }\n const problems: string[] = [];\n const complete = config.properties.every(isPlainProperty);\n if (!complete) {\n problems.push(`defineBrandlabProject({ ... }) in ${CONFIG_FILE} spreads or computes a member, so the ready gate cannot read it in full; write every field literally.`);\n }\n\n const projectId = literal(property(config, \"projectId\"));\n\n const siteSystemNode = property(config, \"siteSystem\");\n const siteSystem = siteSystemNode === undefined ? null : readSiteSystem(literal(siteSystemNode));\n\n const exceptionsNode = property(config, \"exceptions\");\n let exceptions: SiteSystemException[] = [];\n if (exceptionsNode !== undefined) {\n const value = literal(exceptionsNode);\n if (!Array.isArray(value)) {\n problems.push(`The exceptions in ${CONFIG_FILE} are not written as a literal list, so none were applied.`);\n } else {\n exceptions = value.flatMap((entry, index) => {\n const exception = readException(entry);\n if (!exception) problems.push(`exceptions[${index}] in ${CONFIG_FILE} has no literal ruleId, rationale, evidence and reviewTrigger, so it was not applied.`);\n return exception ? [exception] : [];\n });\n }\n }\n\n const searchCertification = literal(property(config, \"searchCertification\"));\n const routes = isRecord(searchCertification) ? searchCertification.representativeRoutes : undefined;\n const representativeRoutes: Record<string, string> = {};\n if (isRecord(routes)) {\n for (const [name, path] of Object.entries(routes)) {\n if (typeof path === \"string\" && path.startsWith(\"/\")) representativeRoutes[name] = path;\n }\n } else if (property(config, \"searchCertification\") !== undefined) {\n problems.push(`searchCertification.representativeRoutes in ${CONFIG_FILE} is not literal, so only the home page was sampled from it.`);\n }\n\n return {\n projectId: typeof projectId === \"string\" ? projectId : null,\n siteSystem,\n exceptions,\n representativeRoutes,\n hosting: hostingRecord(config),\n complete,\n problems,\n };\n}\n\n/**\n * `hosting` from brandlab.config.ts text, without throwing: absent when there is no literal defineBrandlabProject\n * call or no hosting member. Engineering checks and the ready gate (W5, W10) read it this way.\n */\nexport function parseProjectHosting(text: string): SiteSystemHostingRecord {\n const source = ts.createSourceFile(CONFIG_FILE, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);\n const config = findProjectObject(source);\n return config ? hostingRecord(config) : { state: \"absent\" };\n}\n\nfunction hostingRecord(config: ts.ObjectLiteralExpression): SiteSystemHostingRecord {\n const node = property(config, \"hosting\");\n if (node === undefined) return { state: \"absent\" };\n const value = literal(node);\n return value === NOT_LITERAL ? { state: \"computed\" } : { state: \"literal\", value };\n}\n\n/** `key: value` or `key` with an identifier or string key; spreads, computed keys and methods are not. */\nfunction isPlainProperty(member: ts.ObjectLiteralElementLike): boolean {\n if (ts.isShorthandPropertyAssignment(member)) return !member.objectAssignmentInitializer;\n return ts.isPropertyAssignment(member) && (ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name));\n}\n\nfunction readSiteSystem(value: unknown): SiteSystemSettings {\n if (value === NOT_LITERAL || !isRecord(value)) {\n throw new Error(`siteSystem in ${CONFIG_FILE} must be written as a literal object, for example siteSystem: { tier: 'shell' }.`);\n }\n const unknown = Object.keys(value).filter((key) => !SITE_SYSTEM_KEYS.has(key));\n if (unknown.length > 0) throw new Error(`siteSystem in ${CONFIG_FILE} contains unsupported keys: ${unknown.join(\", \")}.`);\n if (!SITE_SYSTEM_TIERS.includes(value.tier as SiteSystemTier)) {\n throw new Error(`siteSystem.tier in ${CONFIG_FILE} must be ${SITE_SYSTEM_TIERS.join(\", \")}.`);\n }\n if (value.newBuild !== undefined && typeof value.newBuild !== \"boolean\") {\n throw new Error(`siteSystem.newBuild in ${CONFIG_FILE} must be true or false.`);\n }\n if (\n value.samples !== undefined &&\n (!Array.isArray(value.samples) || value.samples.some((path) => typeof path !== \"string\" || !path.startsWith(\"/\")))\n ) {\n throw new Error(`siteSystem.samples in ${CONFIG_FILE} must be a literal list of root-relative paths.`);\n }\n if (value.freeQuotes !== undefined && typeof value.freeQuotes !== \"boolean\") {\n throw new Error(`siteSystem.freeQuotes in ${CONFIG_FILE} must be true or false.`);\n }\n return {\n tier: value.tier as SiteSystemTier,\n newBuild: value.newBuild === true,\n samples: (value.samples as string[] | undefined) ?? [],\n labels: stringList(value.labels, \"labels\", \"non-empty labels\", (label) => label.trim().length > 0),\n freeQuotes: (value.freeQuotes as boolean | undefined) ?? null,\n thirdParties: stringList(value.thirdParties, \"thirdParties\", \"hostnames such as www.youtube.com\", (host) => HOSTNAME_PATTERN.test(host)),\n phones: stringList(value.phones, \"phones\", \"tel: links such as tel:+61290001234\", (href) => TEL_HREF_PATTERN.test(href)),\n ...(value.refreshExceptions !== undefined ? { refreshExceptions: refreshExceptions(value.refreshExceptions) } : {}),\n ...(value.replyTime !== undefined ? { replyTime: replyTime(value.replyTime) } : {}),\n };\n}\n\n/** The C7 reply-time record, as defineBrandlabProject validates it; anything else is a configuration error. */\nfunction replyTime(value: unknown): SiteSystemReplyTime {\n const expected = `siteSystem.replyTime in ${CONFIG_FILE} must be { promise: true } or { promise: false, wording, decidedBy: \"owner\" | \"romie\", decidedOn: \"YYYY-MM-DD\" }, with wording that promises no time`;\n if (!isRecord(value)) throw new Error(`${expected}.`);\n if (value.promise === true && Object.keys(value).length === 1) return { promise: true };\n const { promise, wording, decidedBy, decidedOn } = value;\n if (\n promise !== false ||\n Object.keys(value).some((key) => ![\"promise\", \"wording\", \"decidedBy\", \"decidedOn\"].includes(key)) ||\n typeof wording !== \"string\" ||\n !wording.trim() ||\n SITE_SYSTEM_REPLY_TIME_PROMISE.test(wording) ||\n (decidedBy !== \"owner\" && decidedBy !== \"romie\") ||\n !isIsoDay(decidedOn)\n ) {\n throw new Error(`${expected}.`);\n }\n return { promise: false, wording, decidedBy, decidedOn: decidedOn as string };\n}\n\n/** A real YYYY-MM-DD calendar day. */\nfunction isIsoDay(value: unknown): boolean {\n if (typeof value !== \"string\" || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n const [year, month, day] = value.split(\"-\").map(Number) as [number, number, number];\n return new Date(Date.UTC(year, month - 1, day)).toISOString().slice(0, 10) === value;\n}\n\n/** The W3 exceptions, as defineBrandlabProject validates them; anything else is a configuration error. */\nfunction refreshExceptions(value: unknown): SiteSystemRefreshException[] {\n const expected = `siteSystem.refreshExceptions in ${CONFIG_FILE} must be a literal list of { family, revalidateSeconds, reason, decisionId }`;\n if (!Array.isArray(value)) throw new Error(`${expected}.`);\n const families = new Set<string>();\n return value.map((entry, index) => {\n if (!isRecord(entry) || Object.keys(entry).some((key) => ![\"family\", \"revalidateSeconds\", \"reason\", \"decisionId\"].includes(key))) {\n throw new Error(`${expected}; entry ${index} is not.`);\n }\n const { family, revalidateSeconds, reason, decisionId } = entry;\n if (\n !(SITE_SYSTEM_ESTATE_FAMILIES as readonly unknown[]).includes(family) ||\n !Number.isSafeInteger(revalidateSeconds) ||\n (revalidateSeconds as number) < 60 ||\n (revalidateSeconds as number) >= 604_800 ||\n typeof reason !== \"string\" ||\n !reason.trim() ||\n typeof decisionId !== \"string\" ||\n !decisionId.trim() ||\n families.has(family as string)\n ) {\n 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.`);\n }\n families.add(family as string);\n return { family: family as SiteSystemRefreshException[\"family\"], revalidateSeconds: revalidateSeconds as number, reason, decisionId };\n });\n}\n\n/** A literal list of unique strings that each pass `valid`, or [] when absent. */\nfunction stringList(value: unknown, key: string, expected: string, valid: (item: string) => boolean): string[] {\n if (value === undefined) return [];\n if (\n !Array.isArray(value) ||\n value.some((item) => typeof item !== \"string\" || !valid(item)) ||\n new Set(value).size !== value.length\n ) {\n throw new Error(`siteSystem.${key} in ${CONFIG_FILE} must be a literal list of unique ${expected}.`);\n }\n return value as string[];\n}\n\nfunction readException(value: unknown): SiteSystemException | null {\n if (!isRecord(value)) return null;\n const { ruleId, rationale, evidence, expiresAt, reviewTrigger } = value;\n if (\n typeof ruleId !== \"string\" || !ruleId.trim() ||\n typeof rationale !== \"string\" ||\n typeof evidence !== \"string\" ||\n typeof reviewTrigger !== \"string\" ||\n (expiresAt !== undefined && typeof expiresAt !== \"string\")\n ) {\n return null;\n }\n return { ruleId, rationale, evidence, reviewTrigger, ...(expiresAt !== undefined ? { expiresAt } : {}) };\n}\n\nfunction findProjectObject(source: ts.SourceFile): ts.ObjectLiteralExpression | undefined {\n let found: ts.ObjectLiteralExpression | undefined;\n const visit = (node: ts.Node) => {\n if (found) return;\n if (ts.isCallExpression(node)) {\n const callee = node.expression;\n const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : null;\n const argument = node.arguments[0] ? unwrap(node.arguments[0]) : undefined;\n if (name === \"defineBrandlabProject\" && argument && ts.isObjectLiteralExpression(argument)) {\n found = argument;\n return;\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(source);\n return found;\n}\n\nfunction property(object: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined {\n for (const member of object.properties) {\n if (ts.isShorthandPropertyAssignment(member) && member.name.text === name) return member.name;\n if (!ts.isPropertyAssignment(member)) continue;\n const key = member.name;\n if ((ts.isIdentifier(key) || ts.isStringLiteralLike(key)) && key.text === name) return member.initializer;\n }\n return undefined;\n}\n\nfunction unwrap(node: ts.Expression): ts.Expression {\n let current = node;\n while (\n ts.isParenthesizedExpression(current) ||\n ts.isAsExpression(current) ||\n ts.isSatisfiesExpression(current) ||\n ts.isNonNullExpression(current) ||\n ts.isTypeAssertionExpression(current)\n ) {\n current = current.expression;\n }\n return current;\n}\n\n/** The JSON value of a literal expression, or NOT_LITERAL when any part is computed. */\nfunction literal(node: ts.Expression | undefined): unknown {\n if (!node) return undefined;\n const value = unwrap(node);\n if (ts.isStringLiteralLike(value)) return value.text;\n if (ts.isNumericLiteral(value)) return Number(value.text);\n if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)) {\n return -Number(value.operand.text);\n }\n if (value.kind === ts.SyntaxKind.TrueKeyword) return true;\n if (value.kind === ts.SyntaxKind.FalseKeyword) return false;\n if (value.kind === ts.SyntaxKind.NullKeyword) return null;\n if (ts.isArrayLiteralExpression(value)) {\n const items = value.elements.map((element) => (ts.isSpreadElement(element) ? NOT_LITERAL : literal(element)));\n return items.includes(NOT_LITERAL) ? NOT_LITERAL : items;\n }\n if (ts.isObjectLiteralExpression(value)) {\n const object: Record<string, unknown> = {};\n for (const member of value.properties) {\n if (!ts.isPropertyAssignment(member)) return NOT_LITERAL;\n const key = member.name;\n if (!ts.isIdentifier(key) && !ts.isStringLiteralLike(key)) return NOT_LITERAL;\n const item = literal(member.initializer);\n if (item === NOT_LITERAL) return NOT_LITERAL;\n object[key.text] = item;\n }\n return object;\n }\n return NOT_LITERAL;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n"]}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* records and the ready report.
|
|
4
4
|
*/
|
|
5
5
|
export { SITE_SYSTEM_CHECKS, SITE_SYSTEM_TIERS, siteSystemCheckBlocks, type SiteSystemCheck, type SiteSystemCheckId, type SiteSystemCheckKind, type SiteSystemCheckScope, type SiteSystemFinding, type SiteSystemTier, } from "./checks.js";
|
|
6
|
-
export { parseProjectHosting, parseSiteSystemProjectConfig, readSiteSystemProjectConfig, type SiteSystemException, type SiteSystemHostingRecord, type SiteSystemProjectConfig, type SiteSystemSettings, } from "./config.js";
|
|
6
|
+
export { parseProjectHosting, parseSiteSystemProjectConfig, readSiteSystemProjectConfig, type SiteSystemException, type SiteSystemHostingRecord, SITE_SYSTEM_REPLY_TIME_PROMISE, type SiteSystemProjectConfig, type SiteSystemReplyTime, type SiteSystemSettings, } from "./config.js";
|
|
7
7
|
export { readSiteSystemJourneyReport, siteSystemCheckIdsInTitle, type SiteSystemJourneyResults, } from "./playwright-report.js";
|
|
8
8
|
export { appendSiteSystemRecord, latestSiteSystemRecords, readSiteSystemRecords, SITE_SYSTEM_RECORD_PATHS, SITE_SYSTEM_RECORDS_SCHEMA, type SiteSystemRecord, type SiteSystemRecordsDocument, } from "./records.js";
|
|
9
9
|
export { buildSiteSystemReadyReport, isExceptionExpired, renderSiteSystemReadyMarkdown, SITE_SYSTEM_READY_REPORT_PATHS, SITE_SYSTEM_READY_SCHEMA, type BuildSiteSystemReadyReportInput, type SiteSystemReadyCheck, type SiteSystemReadyException, type SiteSystemReadyInputs, type SiteSystemReadyReport, type SiteSystemReadyStatus, type SiteSystemReadyStep, type SiteSystemReadyVerdict, type SiteSystemVerifyResult, } from "./report.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,8BAA8B,EAC9B,wBAAwB,EACxB,KAAK,+BAA+B,EACpC,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,8BAA8B,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,8BAA8B,EAC9B,wBAAwB,EACxB,KAAK,+BAA+B,EACpC,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* records and the ready report.
|
|
4
4
|
*/
|
|
5
5
|
export { SITE_SYSTEM_CHECKS, SITE_SYSTEM_TIERS, siteSystemCheckBlocks, } from "./checks.js";
|
|
6
|
-
export { parseProjectHosting, parseSiteSystemProjectConfig, readSiteSystemProjectConfig, } from "./config.js";
|
|
6
|
+
export { parseProjectHosting, parseSiteSystemProjectConfig, readSiteSystemProjectConfig, SITE_SYSTEM_REPLY_TIME_PROMISE, } from "./config.js";
|
|
7
7
|
export { readSiteSystemJourneyReport, siteSystemCheckIdsInTitle, } from "./playwright-report.js";
|
|
8
8
|
export { appendSiteSystemRecord, latestSiteSystemRecords, readSiteSystemRecords, SITE_SYSTEM_RECORD_PATHS, SITE_SYSTEM_RECORDS_SCHEMA, } from "./records.js";
|
|
9
9
|
export { buildSiteSystemReadyReport, isExceptionExpired, renderSiteSystemReadyMarkdown, SITE_SYSTEM_READY_REPORT_PATHS, SITE_SYSTEM_READY_SCHEMA, } from "./report.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,GAOtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,2BAA2B,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/site-system/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,GAOtB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,2BAA2B,EAG3B,8BAA8B,GAI/B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,GAE1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,0BAA0B,GAG3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,0BAA0B,EAC1B,kBAAkB,EAClB,6BAA6B,EAC7B,8BAA8B,EAC9B,wBAAwB,GAUzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EACL,mBAAmB,EACnB,4BAA4B,GAG7B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,GAIpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,EAC9B,oBAAoB,GAKrB,MAAM,YAAY,CAAC","sourcesContent":["/**\n * The Site System v2 ready gate: the check registry, the rule-module interface `brandlab-ready` runs, the step\n * records and the ready report.\n */\nexport {\n SITE_SYSTEM_CHECKS,\n SITE_SYSTEM_TIERS,\n siteSystemCheckBlocks,\n type SiteSystemCheck,\n type SiteSystemCheckId,\n type SiteSystemCheckKind,\n type SiteSystemCheckScope,\n type SiteSystemFinding,\n type SiteSystemTier,\n} from \"./checks.js\";\nexport {\n parseProjectHosting,\n parseSiteSystemProjectConfig,\n readSiteSystemProjectConfig,\n type SiteSystemException,\n type SiteSystemHostingRecord,\n SITE_SYSTEM_REPLY_TIME_PROMISE,\n type SiteSystemProjectConfig,\n type SiteSystemReplyTime,\n type SiteSystemSettings,\n} from \"./config.js\";\nexport {\n readSiteSystemJourneyReport,\n siteSystemCheckIdsInTitle,\n type SiteSystemJourneyResults,\n} from \"./playwright-report.js\";\nexport {\n appendSiteSystemRecord,\n latestSiteSystemRecords,\n readSiteSystemRecords,\n SITE_SYSTEM_RECORD_PATHS,\n SITE_SYSTEM_RECORDS_SCHEMA,\n type SiteSystemRecord,\n type SiteSystemRecordsDocument,\n} from \"./records.js\";\nexport {\n buildSiteSystemReadyReport,\n isExceptionExpired,\n renderSiteSystemReadyMarkdown,\n SITE_SYSTEM_READY_REPORT_PATHS,\n SITE_SYSTEM_READY_SCHEMA,\n type BuildSiteSystemReadyReportInput,\n type SiteSystemReadyCheck,\n type SiteSystemReadyException,\n type SiteSystemReadyInputs,\n type SiteSystemReadyReport,\n type SiteSystemReadyStatus,\n type SiteSystemReadyStep,\n type SiteSystemReadyVerdict,\n type SiteSystemVerifyResult,\n} from \"./report.js\";\nexport { SITE_SYSTEM_RENDERED_RUNNERS } from \"./rule-modules.js\";\nexport {\n isSiteSystemFinding,\n runSiteSystemRenderedRunners,\n type SiteSystemRenderedInput,\n type SiteSystemRenderedRunner,\n} from \"./runners.js\";\nexport {\n builtPageFile,\n loadSiteSystemPages,\n planSiteSystemSamplePaths,\n resolveStreamedHtml,\n type SiteSystemFetch,\n type SiteSystemPage,\n type SiteSystemPageLoad,\n} from \"./samples.js\";\nexport {\n SITE_SYSTEM_HUMAN_STEPS,\n SITE_SYSTEM_LAUNCH_STEPS,\n SITE_SYSTEM_RECORDED_CHECK_IDS,\n siteSystemRecordFile,\n type SiteSystemHumanStepId,\n type SiteSystemLaunchStepId,\n type SiteSystemRecordFile,\n type SiteSystemStep,\n} from \"./steps.js\";\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversion.d.ts","sourceRoot":"","sources":["../../../src/site-system/journeys/conversion.ts"],"names":[],"mappings":"AAWA,OAAO,EAA6F,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAMlJ,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"conversion.d.ts","sourceRoot":"","sources":["../../../src/site-system/journeys/conversion.ts"],"names":[],"mappings":"AAWA,OAAO,EAA6F,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAMlJ,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAsIrG"}
|
|
@@ -88,14 +88,23 @@ export function defineConversionJourneys(plan, runs) {
|
|
|
88
88
|
let analytics;
|
|
89
89
|
let taps = 0;
|
|
90
90
|
let declared = 0;
|
|
91
|
+
const skipped = [];
|
|
91
92
|
const tap = async (mark, link, where) => {
|
|
92
93
|
declared += 1;
|
|
93
94
|
if (link.invalid)
|
|
94
95
|
problems.push(`${where}: ${link.description} declares data-phone-location "${link.invalid}", which is not an allowed placement (${SITE_SYSTEM_PHONE_PLACEMENTS.join(', ')}); its taps are reported as ${link.placement}`);
|
|
95
96
|
if (!analytics)
|
|
96
97
|
return;
|
|
98
|
+
// An earlier tap can hide a link that showed when the page was read (a footer tap scrolls the footer in and
|
|
99
|
+
// the call bar hides over it): a link nobody can tap now is left out, not waited on.
|
|
100
|
+
const target = page.locator(`[data-brandlab-journey~="${mark}"]`).first();
|
|
101
|
+
const tappable = () => target.evaluate((element) => window.__brandlabSiteSystem.isRendered(element) && !element.closest('[inert]')).catch(() => false);
|
|
102
|
+
if (!(await waitUntil(tappable, 1_000))) {
|
|
103
|
+
skipped.push(`${where}: ${link.description}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
97
106
|
const before = analyticsEvents(commands, 'phone_link_click').length;
|
|
98
|
-
await
|
|
107
|
+
await target.click();
|
|
99
108
|
await waitUntil(async () => analyticsEvents(commands, 'phone_link_click').length > before, 2_000);
|
|
100
109
|
const sent = analyticsEvents(commands, 'phone_link_click').slice(before);
|
|
101
110
|
taps += 1;
|
|
@@ -116,7 +125,9 @@ export function defineConversionJourneys(plan, runs) {
|
|
|
116
125
|
const where = `${viewport.width} px`;
|
|
117
126
|
const links = await page.evaluate(() => {
|
|
118
127
|
const helpers = window.__brandlabSiteSystem;
|
|
119
|
-
|
|
128
|
+
// Below 1024 px the call bar's links are tapped in the call-bar step, which scrolls until the bar shows.
|
|
129
|
+
const bar = window.innerWidth < 1024 ? helpers.findCallBar() : null;
|
|
130
|
+
return Array.from(document.querySelectorAll('a[href^="tel:"]')).filter((link) => helpers.isRendered(link) && !bar?.contains(link)).map((link, index) => {
|
|
120
131
|
helpers.markElement(link, `tel-${index}`);
|
|
121
132
|
return { mark: `tel-${index}`, description: helpers.describeElement(link), ...helpers.phonePlacement(link) };
|
|
122
133
|
});
|
|
@@ -144,6 +155,8 @@ export function defineConversionJourneys(plan, runs) {
|
|
|
144
155
|
note(testInfo, `${path}: the build sends no analytics (no Google tag ran), so the declared placements were checked without tapping; build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID to check the events`);
|
|
145
156
|
else
|
|
146
157
|
note(testInfo, `${path}: ${taps} phone tap(s) checked against their phone_link_click events`);
|
|
158
|
+
if (skipped.length > 0)
|
|
159
|
+
note(testInfo, `${path}: not tapped, hidden by the time their turn came (their declared placements were checked): ${skipped.join('; ')}`);
|
|
147
160
|
expect(problems).toEqual([]);
|
|
148
161
|
});
|
|
149
162
|
}
|