@brandlab-digital/conformance 3.12.1 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +41 -1
  2. package/dist/analytics-e2e.d.ts +10 -16
  3. package/dist/analytics-e2e.d.ts.map +1 -1
  4. package/dist/analytics-e2e.js +61 -3
  5. package/dist/analytics-e2e.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/current-release.js +5 -5
  10. package/dist/current-release.js.map +1 -1
  11. package/dist/edge-probe-cli.d.ts +57 -0
  12. package/dist/edge-probe-cli.d.ts.map +1 -0
  13. package/dist/edge-probe-cli.js +134 -0
  14. package/dist/edge-probe-cli.js.map +1 -0
  15. package/dist/engineering.d.ts +4 -1
  16. package/dist/engineering.d.ts.map +1 -1
  17. package/dist/engineering.js +19 -14
  18. package/dist/engineering.js.map +1 -1
  19. package/dist/estate-sample.d.ts +60 -0
  20. package/dist/estate-sample.d.ts.map +1 -0
  21. package/dist/estate-sample.js +144 -0
  22. package/dist/estate-sample.js.map +1 -0
  23. package/dist/estate.d.ts +45 -0
  24. package/dist/estate.d.ts.map +1 -1
  25. package/dist/estate.js +82 -22
  26. package/dist/estate.js.map +1 -1
  27. package/dist/origin-region.d.ts +14 -0
  28. package/dist/origin-region.d.ts.map +1 -0
  29. package/dist/origin-region.js +46 -0
  30. package/dist/origin-region.js.map +1 -0
  31. package/dist/prefetch-probe-cli.d.ts +36 -0
  32. package/dist/prefetch-probe-cli.d.ts.map +1 -0
  33. package/dist/prefetch-probe-cli.js +163 -0
  34. package/dist/prefetch-probe-cli.js.map +1 -0
  35. package/dist/project.d.ts +20 -0
  36. package/dist/project.d.ts.map +1 -1
  37. package/dist/project.js +92 -2
  38. package/dist/project.js.map +1 -1
  39. package/dist/ready-cli.d.ts.map +1 -1
  40. package/dist/ready-cli.js +8 -3
  41. package/dist/ready-cli.js.map +1 -1
  42. package/dist/rule-registry.d.ts +5 -0
  43. package/dist/rule-registry.d.ts.map +1 -1
  44. package/dist/rule-registry.js +1 -0
  45. package/dist/rule-registry.js.map +1 -1
  46. package/dist/site-system/checks.d.ts +62 -1
  47. package/dist/site-system/checks.d.ts.map +1 -1
  48. package/dist/site-system/checks.js +9 -0
  49. package/dist/site-system/checks.js.map +1 -1
  50. package/dist/site-system/config.d.ts +27 -0
  51. package/dist/site-system/config.d.ts.map +1 -1
  52. package/dist/site-system/config.js +50 -1
  53. package/dist/site-system/config.js.map +1 -1
  54. package/dist/site-system/index.d.ts +1 -1
  55. package/dist/site-system/index.d.ts.map +1 -1
  56. package/dist/site-system/index.js +1 -1
  57. package/dist/site-system/index.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +9 -2
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +11 -1
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
  63. package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
  64. package/dist/site-system/journeys/prefetch-measure.js +94 -0
  65. package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
  66. package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
  67. package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
  68. package/dist/site-system/journeys/prefetch-policy.js +152 -0
  69. package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
  70. package/dist/site-system/journeys/prefetch.d.ts +3 -0
  71. package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
  72. package/dist/site-system/journeys/prefetch.js +37 -0
  73. package/dist/site-system/journeys/prefetch.js.map +1 -0
  74. package/dist/site-system/journeys/variants.d.ts +34 -0
  75. package/dist/site-system/journeys/variants.d.ts.map +1 -0
  76. package/dist/site-system/journeys/variants.js +139 -0
  77. package/dist/site-system/journeys/variants.js.map +1 -0
  78. package/dist/site-system/report.d.ts.map +1 -1
  79. package/dist/site-system/report.js +19 -5
  80. package/dist/site-system/report.js.map +1 -1
  81. package/dist/site-system/rule-modules.d.ts.map +1 -1
  82. package/dist/site-system/rule-modules.js +10 -0
  83. package/dist/site-system/rule-modules.js.map +1 -1
  84. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  85. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  86. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  87. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  88. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  89. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  90. package/dist/site-system/rules/delivery/index.js +7 -0
  91. package/dist/site-system/rules/delivery/index.js.map +1 -0
  92. package/dist/site-system/rules/full/common.d.ts +3 -0
  93. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  94. package/dist/site-system/rules/full/common.js.map +1 -1
  95. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  96. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  97. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  98. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  99. package/dist/site-system/rules/full/estate.d.ts +20 -1
  100. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  101. package/dist/site-system/rules/full/estate.js +33 -5
  102. package/dist/site-system/rules/full/estate.js.map +1 -1
  103. package/dist/site-system/rules/full/geography.d.ts +8 -2
  104. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  105. package/dist/site-system/rules/full/geography.js +6 -2
  106. package/dist/site-system/rules/full/geography.js.map +1 -1
  107. package/dist/site-system/rules/full/index.d.ts +3 -2
  108. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  109. package/dist/site-system/rules/full/index.js +27 -9
  110. package/dist/site-system/rules/full/index.js.map +1 -1
  111. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  112. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  113. package/dist/site-system/rules/full/k12-slots.js +40 -0
  114. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  115. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  116. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  117. package/dist/site-system/rules/full/loc-index.js +43 -2
  118. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  119. package/dist/site-system/rules/full/strategy.d.ts +4 -0
  120. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  121. package/dist/site-system/rules/full/strategy.js +19 -3
  122. package/dist/site-system/rules/full/strategy.js.map +1 -1
  123. package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
  124. package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
  125. package/dist/site-system/rules/full/w3-refresh.js +70 -0
  126. package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
  127. package/dist/site-system/rules/guards/index.d.ts +22 -0
  128. package/dist/site-system/rules/guards/index.d.ts.map +1 -0
  129. package/dist/site-system/rules/guards/index.js +109 -0
  130. package/dist/site-system/rules/guards/index.js.map +1 -0
  131. package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
  132. package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
  133. package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
  134. package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
  135. package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
  136. package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
  137. package/dist/site-system/rules/guards/w9-params.js +84 -0
  138. package/dist/site-system/rules/guards/w9-params.js.map +1 -0
  139. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  140. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  141. package/dist/site-system/rules/sections/k2-skin-coverage.js +6 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/runners.js +1 -1
  144. package/dist/site-system/runners.js.map +1 -1
  145. package/dist/site-system-e2e.d.ts +1 -0
  146. package/dist/site-system-e2e.d.ts.map +1 -1
  147. package/dist/site-system-e2e.js +8 -2
  148. package/dist/site-system-e2e.js.map +1 -1
  149. package/package.json +6 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estate-sample.js","sourceRoot":"","sources":["../src/estate-sample.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The sampled estate crawl (3.13.0, client CI v2). A draft pull request checks a\n * sample of the catalogue instead of every route: the hubs, the first and last\n * page of each route kind, the routes a changed data file feeds and a fixed-seed\n * sample of the rest. The full crawl still runs when the pull request is ready\n * for review and before merge; the sample only makes drafts cheaper.\n *\n * CI chooses the scope with `BRANDLAB_ESTATE_SCOPE` (`full`, the default, or\n * `sample`) and lists the changed files in the file `BRANDLAB_CHANGED_FILES`\n * names, one path per line. Any other scope value runs the full crawl.\n */\nimport { createHash } from 'node:crypto';\nimport { readFileSync } from 'node:fs';\n\nexport type BrandlabEstateScope = 'full' | 'sample';\n\n/** The route fields the sample reads; a catalogue route has them. */\nexport interface BrandlabEstateSampleRoute {\n path: string;\n kind?: string;\n id?: string;\n}\n\nexport interface BrandlabEstateSampleOptions {\n /** How many more routes the fixed seed picks beyond hubs, kind ends and changed routes (default 50). */\n size?: number;\n /** The seed; the same seed and catalogue always give the same sample (default `brandlab-estate-sample`). */\n seed?: string;\n /**\n * The routes a changed file feeds, as paths. By default a route is fed by a file\n * named after its id or its last path segment (`src/data/locations/bondi.ts`\n * feeds `/locations/bondi`; an `index` file is named by its folder).\n */\n routesForFile?: (file: string, routes: readonly BrandlabEstateSampleRoute[]) => readonly string[];\n}\n\nexport interface BrandlabEstateSample<T extends BrandlabEstateSampleRoute> {\n scope: BrandlabEstateScope;\n /** The routes to crawl, in catalogue order. */\n routes: T[];\n total: number;\n counts: { hubs: number; kindEnds: number; changed: number; seeded: number };\n /** One line for the test annotation. */\n summary: string;\n}\n\n/** Hubs: home, every index page, the contact page and the not-found page. */\nconst HUB_KINDS = new Set(['home', 'service-index', 'location-index', 'blog-index', 'project-index', 'contact']);\nconst DEFAULT_SIZE = 50;\nconst DEFAULT_SEED = 'brandlab-estate-sample';\nconst GENERIC_STEMS = new Set(['index', 'data', 'content', 'page']);\n\n/** The scope CI asked for. Anything but `sample` is the full crawl, so a typo never thins a check. */\nexport function estateScopeFromEnv(env: NodeJS.ProcessEnv = process.env): { scope: BrandlabEstateScope; note?: string } {\n const value = (env.BRANDLAB_ESTATE_SCOPE ?? '').trim();\n if (value === 'sample') return { scope: 'sample' };\n if (value === '' || value === 'full') return { scope: 'full' };\n return { scope: 'full', note: `BRANDLAB_ESTATE_SCOPE=\"${value}\" is not full or sample; the full estate was crawled` };\n}\n\n/** The changed files CI listed (`BRANDLAB_CHANGED_FILES` names the list). Unreadable or unset lists none. */\nexport function changedFilesFromEnv(env: NodeJS.ProcessEnv = process.env): string[] {\n const listPath = env.BRANDLAB_CHANGED_FILES?.trim();\n if (!listPath) return [];\n try {\n return readFileSync(listPath, 'utf8').split('\\n').map((line) => line.trim()).filter(Boolean);\n } catch {\n return [];\n }\n}\n\n/** The name a data file gives the routes it feeds: its file name without extensions, or its folder's for an index file. */\nexport function changedFileStem(file: string): string | undefined {\n const parts = file.replace(/\\\\/g, '/').split('/').filter(Boolean);\n const stemOf = (part: string | undefined) => part?.replace(/(?:\\.[a-z0-9]+)+$/i, '').toLowerCase() ?? '';\n const own = stemOf(parts.at(-1));\n const stem = GENERIC_STEMS.has(own) ? stemOf(parts.at(-2)) : own;\n return stem && !GENERIC_STEMS.has(stem) ? stem : undefined;\n}\n\nexport function defaultRoutesForFile(file: string, routes: readonly BrandlabEstateSampleRoute[]): string[] {\n const stem = changedFileStem(file);\n if (!stem) return [];\n return routes\n .filter((route) => route.id?.toLowerCase() === stem || lastSegment(route.path) === stem)\n .map((route) => route.path);\n}\n\n/**\n * The routes a sampled crawl checks. The full scope returns every route. The\n * sample keeps the hubs and the not-found page, the first and last route of each\n * kind in catalogue order, the routes the changed files feed and `size` more\n * chosen by a hash of the seed and the path, so the same catalogue always gives\n * the same sample.\n */\nexport function sampleEstateRoutes<T extends BrandlabEstateSampleRoute>(\n routes: readonly T[],\n input: { scope: BrandlabEstateScope; changedFiles?: readonly string[] } & BrandlabEstateSampleOptions,\n): BrandlabEstateSample<T> {\n const total = routes.length;\n if (input.scope === 'full') {\n return { scope: 'full', routes: [...routes], total, counts: { hubs: 0, kindEnds: 0, changed: 0, seeded: 0 }, summary: `full: all ${total} routes` };\n }\n const chosen = new Set<string>();\n const add = (path: string) => {\n const key = normalise(path);\n if (chosen.has(key)) return false;\n chosen.add(key);\n return true;\n };\n let hubs = 0;\n for (const route of routes) {\n if (HUB_KINDS.has(route.kind ?? '') || normalise(route.path) === '/' || normalise(route.path) === '/404') if (add(route.path)) hubs += 1;\n }\n const byKind = new Map<string, T[]>();\n for (const route of routes) {\n if (HUB_KINDS.has(route.kind ?? '')) continue;\n const kind = route.kind ?? '';\n byKind.set(kind, [...(byKind.get(kind) ?? []), route]);\n }\n let kindEnds = 0;\n for (const group of byKind.values()) {\n for (const route of [group[0], group.at(-1)]) if (route && add(route.path)) kindEnds += 1;\n }\n const known = new Set(routes.map((route) => normalise(route.path)));\n const feeds = input.routesForFile ?? defaultRoutesForFile;\n let changed = 0;\n for (const file of input.changedFiles ?? []) {\n for (const path of feeds(file, routes)) if (known.has(normalise(path)) && add(path)) changed += 1;\n }\n const seed = input.seed ?? DEFAULT_SEED;\n const size = Math.max(0, Math.floor(input.size ?? DEFAULT_SIZE));\n const rest = routes\n .filter((route) => !chosen.has(normalise(route.path)))\n .map((route) => ({ path: route.path, rank: seededRank(seed, normalise(route.path)) }))\n .sort((a, b) => a.rank.localeCompare(b.rank) || a.path.localeCompare(b.path))\n .slice(0, size);\n for (const { path } of rest) add(path);\n const sampled = routes.filter((route) => chosen.has(normalise(route.path)));\n // A path the catalogue repeats is checked once.\n const unique = sampled.filter((route, index) => sampled.findIndex((other) => normalise(other.path) === normalise(route.path)) === index);\n return {\n scope: 'sample',\n routes: unique,\n total,\n counts: { hubs, kindEnds, changed, seeded: rest.length },\n summary: `sample: ${unique.length} of ${total} routes (hubs ${hubs}, first and last of each kind ${kindEnds}, from changed files ${changed}, fixed-seed ${rest.length}); the full estate is crawled when the pull request is ready for review`,\n };\n}\n\n/** A fixed-seed sample of other items (redirect rules), in their original order. */\nexport function sampleBySeed<T>(items: readonly T[], keyOf: (item: T) => string, input: { size?: number; seed?: string } = {}): T[] {\n const seed = input.seed ?? DEFAULT_SEED;\n const size = Math.max(0, Math.floor(input.size ?? DEFAULT_SIZE));\n const picked = new Set(\n items\n .map((item, index) => ({ index, rank: seededRank(seed, keyOf(item)) }))\n .sort((a, b) => a.rank.localeCompare(b.rank) || a.index - b.index)\n .slice(0, size)\n .map(({ index }) => index),\n );\n return items.filter((_, index) => picked.has(index));\n}\n\nfunction seededRank(seed: string, key: string): string {\n return createHash('sha256').update(`${seed}\\u0000${key}`).digest('hex');\n}\n\nfunction lastSegment(path: string): string {\n return normalise(path).split('/').filter(Boolean).at(-1)?.toLowerCase() ?? '';\n}\n\nfunction normalise(path: string): string {\n const pathname = path.split(/[?#]/)[0] || '/';\n return pathname.length > 1 ? pathname.replace(/\\/+$/, '') : '/';\n}\n"]}
package/dist/estate.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type APIRequestContext } from '@playwright/test';
2
2
  import { type LocationCapacityRecord, type SitemapFile, type SuburbCentreLookup } from './page-standard.js';
3
+ import { type BrandlabEstateSampleOptions } from './estate-sample.js';
4
+ export { changedFilesFromEnv, changedFileStem, defaultRoutesForFile, estateScopeFromEnv, sampleBySeed, sampleEstateRoutes, type BrandlabEstateSample, type BrandlabEstateSampleOptions, type BrandlabEstateSampleRoute, type BrandlabEstateScope, } from './estate-sample.js';
3
5
  export interface BrandlabEstateRoute {
4
6
  path: string;
5
7
  canonicalPath?: string;
@@ -7,6 +9,8 @@ export interface BrandlabEstateRoute {
7
9
  /** The route kind (a catalogue route has it): sitemap families and location checks read it. */
8
10
  kind?: string;
9
11
  id?: string;
12
+ /** The catalogue parent: a location page other location pages sit under is a region page. */
13
+ parentId?: string;
10
14
  /** The content date the sitemap's lastmod must carry. */
11
15
  lastSignificantChange?: string;
12
16
  locationDecision?: {
@@ -77,8 +81,18 @@ export interface BrandlabEstateTestOptions {
77
81
  };
78
82
  /** A place's centre for neighbour order (default: the registry's suburb centres). */
79
83
  centreOf?: SuburbCentreLookup;
84
+ /**
85
+ * The site's estate plan (site-core `siteStrategy.estate`, 3.14.0): its public slugs map a location page to the
86
+ * registry suburb it names (`ryde-suburb` → `ryde`) and mark region pages (`ryde`), which are not ranked by distance.
87
+ */
88
+ estate?: BrandlabEstateSlugPlan | null;
80
89
  /** The most inline markup one service-area map may carry (default 60 000 bytes, 3.6.0). */
81
90
  areaMapMaxBytes?: number;
91
+ /**
92
+ * The sampled crawl a draft pull request runs (`BRANDLAB_ESTATE_SCOPE=sample`, 3.13.0): how many routes and
93
+ * redirects the fixed seed adds, the seed, and which routes a changed file feeds.
94
+ */
95
+ sample?: BrandlabEstateSampleOptions;
82
96
  concurrency?: number;
83
97
  timeout?: number;
84
98
  }
@@ -91,6 +105,8 @@ export interface BrandlabPageQualityTestOptions {
91
105
  businessId?: string | false;
92
106
  /** A place's centre for similarity siblings (default: the registry's suburb centres). */
93
107
  centreOf?: SuburbCentreLookup;
108
+ /** The site's estate plan, as for the estate journey: public slugs resolve to the places they name. */
109
+ estate?: BrandlabEstateSlugPlan | null;
94
110
  concurrency?: number;
95
111
  timeout?: number;
96
112
  }
@@ -99,6 +115,25 @@ export interface BrandlabPageQualityFinding {
99
115
  kind: 'headings' | 'title' | 'title-format' | 'title-duplicate' | 'title-h1' | 'description' | 'description-length' | 'description-duplicate' | 'html-size' | 'similarity' | 'location-review' | 'orphan' | 'noindex-link' | 'og-url' | 'implementation-wording' | 'structured-data';
100
116
  detail: string;
101
117
  }
118
+ /** The public-slug part of site-core's SiteEstatePlan (mirrored: conformance compiles without site-core). */
119
+ export interface BrandlabEstateSlugPlan {
120
+ slugs?: {
121
+ /** Registry region slug → public slug, for example `{ 'ryde-area': 'ryde' }`. */
122
+ regions?: Readonly<Record<string, string>>;
123
+ /** Registry suburb slug → public slug, for example `{ ryde: 'ryde-suburb' }`. */
124
+ suburbs?: Readonly<Record<string, string>>;
125
+ };
126
+ }
127
+ /**
128
+ * A location route's place: the registry suburb slug its last path segment names (through the estate plan's public
129
+ * slugs, as site-core's `estatePublicSlug`), and whether it is a region page. A region page has no centre, so its
130
+ * nearby lists are not ranked by distance: it is a region when other location pages sit under it, when its slug is a
131
+ * region's public slug, or when its slug is a suburb's registry slug that the plan moved elsewhere (AA: `ryde`).
132
+ */
133
+ export declare function estatePlaceResolver(routes: readonly BrandlabEstateRoute[], estate?: BrandlabEstateSlugPlan | null): (route: BrandlabEstateRoute | undefined) => {
134
+ slug: string;
135
+ region: boolean;
136
+ } | undefined;
102
137
  export declare function defineBrandlabEstateTests(options: BrandlabEstateTestOptions): void;
103
138
  /**
104
139
  * Why a page's BreadcrumbList JSON-LD would fail Google's breadcrumb rich result (3.3.3): every
@@ -167,6 +202,16 @@ export declare function redirectProbe(rule: Pick<BrandlabEstateRedirect, 'source
167
202
  * defineBrandlabPageQualityTests({ site, routes })
168
203
  */
169
204
  export declare function defineBrandlabPageQualityTests(options: BrandlabPageQualityTestOptions): void;
205
+ /**
206
+ * Whether the advisory page-quality crawl runs (3.13.0, client CI v2). CI v2 runs it on its own when a pull request
207
+ * is ready for review, on a manual run and monthly for large estates, and sets `BRANDLAB_PAGE_QUALITY=skip` for the required journeys so
208
+ * drafts and merges do not crawl every page twice. `BRANDLAB_PAGE_QUALITY_DIR` also writes its reports there.
209
+ */
210
+ export declare function pageQualityRunFromEnv(env?: NodeJS.ProcessEnv): {
211
+ run: boolean;
212
+ reason: string;
213
+ directory?: string;
214
+ };
170
215
  /**
171
216
  * Absolute (`https://host`) and protocol-relative (`//host`) destinations leave
172
217
  * the site; browsers also read `/\host` and `\\host` as protocol-relative.
@@ -1 +1 @@
1
- {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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"}
package/dist/estate.js CHANGED
@@ -35,17 +35,45 @@
35
35
  * links from indexable pages to noindex location pages, centerpiece
36
36
  * similarity (warn at 0.5, review list at 0.7) and the location review list
37
37
  * (location-review.json); pages on that list stay indexed.
38
+ *
39
+ * From 3.13.0 (client CI v2) a draft pull request sets BRANDLAB_ESTATE_SCOPE=sample:
40
+ * the catalogue and redirect journeys check the hubs, the first and last route of
41
+ * each kind, the routes changed files feed and a fixed-seed sample (estate-sample.ts),
42
+ * and say so in an `estate-scope` annotation. Every other run crawls the full estate.
43
+ * BRANDLAB_PAGE_QUALITY=skip skips the advisory crawl, which CI v2 runs on its own.
38
44
  */
45
+ import { mkdirSync, writeFileSync } from 'node:fs';
39
46
  import { request as httpRequest } from 'node:http';
47
+ import { join } from 'node:path';
40
48
  import { expect, test } from '@playwright/test';
41
49
  import { areaMapProblems, buildLocationReviewList, centerpieceSimilarity, checkLocationCapacity, descriptionLengthProblem, HTML_MAX_BYTES, headingTexts, loadBrandlabGeography, neighbourOrderProblems, parseSitemapEntries, SIMILARITY_REVIEW, SIMILARITY_WARN, sitemapFamilyProblems, sitemapLastmodProblems, structuredDataProblems, textBlocks, titleFormatProblems, titleHeadingProblems, } from './page-standard.js';
42
50
  import { extractSearchCertificationHtmlSnapshot } from './search-certification.js';
51
+ import { changedFilesFromEnv, estateScopeFromEnv, sampleBySeed, sampleEstateRoutes } from './estate-sample.js';
52
+ export { changedFilesFromEnv, changedFileStem, defaultRoutesForFile, estateScopeFromEnv, sampleBySeed, sampleEstateRoutes, } from './estate-sample.js';
43
53
  const LOCATION_KINDS = new Set(['location', 'service-location']);
44
- /** A location route's place slug: the last segment of its path. */
45
- function routePlace(route) {
46
- if (!route || !LOCATION_KINDS.has(route.kind ?? ''))
47
- return undefined;
48
- return normalisePath(route.path).split('/').filter(Boolean).at(-1);
54
+ /**
55
+ * A location route's place: the registry suburb slug its last path segment names (through the estate plan's public
56
+ * slugs, as site-core's `estatePublicSlug`), and whether it is a region page. A region page has no centre, so its
57
+ * nearby lists are not ranked by distance: it is a region when other location pages sit under it, when its slug is a
58
+ * region's public slug, or when its slug is a suburb's registry slug that the plan moved elsewhere (AA: `ryde`).
59
+ */
60
+ export function estatePlaceResolver(routes, estate) {
61
+ const parents = new Set(routes.filter((route) => LOCATION_KINDS.has(route.kind ?? '')).map((route) => route.parentId));
62
+ const regionSlugs = new Set(Object.values(estate?.slugs?.regions ?? {}));
63
+ const suburbRenames = estate?.slugs?.suburbs ?? {};
64
+ const suburbByPublic = new Map(Object.entries(suburbRenames).map(([registry, publicSlug]) => [publicSlug, registry]));
65
+ return (route) => {
66
+ if (!route || !LOCATION_KINDS.has(route.kind ?? ''))
67
+ return undefined;
68
+ const publicSlug = normalisePath(route.path).split('/').filter(Boolean).at(-1);
69
+ if (!publicSlug)
70
+ return undefined;
71
+ const suburb = suburbByPublic.get(publicSlug);
72
+ const region = (route.id !== undefined && parents.has(route.id)) ||
73
+ regionSlugs.has(publicSlug) ||
74
+ (suburb === undefined && Object.hasOwn(suburbRenames, publicSlug));
75
+ return { slug: suburb ?? publicSlug, region };
76
+ };
49
77
  }
50
78
  const BUSINESS_TYPES = new Set([
51
79
  'LocalBusiness', 'HomeAndConstructionBusiness', 'GeneralContractor', 'Plumber', 'Electrician',
@@ -71,7 +99,13 @@ export function defineBrandlabEstateTests(options) {
71
99
  const families = options.families ?? [];
72
100
  const familyPaths = new Set(families.flatMap((family) => family.paths.map(normalisePath)));
73
101
  const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
102
+ const placeOfRoute = estatePlaceResolver(routes, options.estate);
74
103
  const homePath = normalisePath(routes.find((route) => route.kind === 'home')?.path ?? '/');
104
+ // A draft pull request crawls a sample (BRANDLAB_ESTATE_SCOPE=sample); ready for review crawls every route.
105
+ const scope = estateScopeFromEnv();
106
+ const sample = sampleEstateRoutes(routes, { scope: scope.scope, changedFiles: changedFilesFromEnv(), ...options.sample });
107
+ const scopeNote = [sample.summary, scope.note].filter(Boolean).join('; ');
108
+ const crawledRedirects = scope.scope === 'sample' ? sampleBySeed(redirects, (rule) => rule.source, options.sample) : redirects;
75
109
  test.describe('Brandlab technical estate', () => {
76
110
  test('the sitemap lists exactly the indexable catalogue routes', async ({ request }) => {
77
111
  test.setTimeout(timeout);
@@ -107,7 +141,8 @@ export function defineBrandlabEstateTests(options) {
107
141
  const internalLinks = new Map();
108
142
  const checked = [];
109
143
  const areaMapLayers = new Map();
110
- await eachWithConcurrency(routes, options.concurrency ?? 8, async (route) => {
144
+ testInfo.annotations.push({ type: 'estate-scope', description: scopeNote });
145
+ await eachWithConcurrency(sample.routes, options.concurrency ?? 8, async (route) => {
111
146
  const [get, head] = await Promise.all([
112
147
  request.get(route.path, { maxRedirects: 0 }),
113
148
  request.head(route.path, { maxRedirects: 0 }),
@@ -158,13 +193,16 @@ export function defineBrandlabEstateTests(options) {
158
193
  business: options.business,
159
194
  }))
160
195
  failures.push(`${route.path}: ${problem}`);
161
- const place = routePlace(route);
162
- if (place && centreOf(place)) {
196
+ const place = placeOfRoute(route);
197
+ if (place && !place.region && centreOf(place.slug)) {
163
198
  for (const problem of neighbourOrderProblems({
164
199
  html,
165
- origin: place,
200
+ origin: place.slug,
166
201
  pageUrl: expectedCanonical,
167
- placeOf: (path) => routePlace(routeByPath.get(path)),
202
+ placeOf: (path) => {
203
+ const linked = placeOfRoute(routeByPath.get(path));
204
+ return linked && !linked.region ? linked.slug : undefined;
205
+ },
168
206
  centreOf,
169
207
  }))
170
208
  failures.push(`${route.path}: ${problem}`);
@@ -223,7 +261,10 @@ export function defineBrandlabEstateTests(options) {
223
261
  test.setTimeout(timeout);
224
262
  const failures = [];
225
263
  const skipped = [];
226
- for (const rule of redirects) {
264
+ if (scope.scope === 'sample') {
265
+ testInfo.annotations.push({ type: 'estate-scope', description: `sample: ${crawledRedirects.length} of ${redirects.length} redirects (fixed seed); every redirect is checked when the pull request is ready for review` });
266
+ }
267
+ for (const rule of crawledRedirects) {
227
268
  const probe = redirectProbe(rule);
228
269
  if (!probe) {
229
270
  skipped.push(`${rule.source} → ${rule.destination}`);
@@ -556,8 +597,11 @@ export function defineBrandlabPageQualityTests(options) {
556
597
  const timeout = options.timeout ?? 240_000;
557
598
  const routes = options.routes.filter((route) => normalisePath(route.path) !== NOT_FOUND_ROUTE);
558
599
  const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
600
+ const placeOfRoute = estatePlaceResolver(routes, options.estate);
601
+ const run = pageQualityRunFromEnv();
559
602
  test.describe('Brandlab page quality (advisory)', () => {
560
603
  test('reports page and structured-data quality without failing', async ({ request }, testInfo) => {
604
+ test.skip(!run.run, run.reason);
561
605
  test.setTimeout(timeout);
562
606
  const findings = [];
563
607
  const geography = await loadBrandlabGeography();
@@ -587,9 +631,9 @@ export function defineBrandlabPageQualityTests(options) {
587
631
  if (formatProblems.length > 0)
588
632
  add('title-format', `"${snapshot.title}": ${formatProblems.join('; ')}`);
589
633
  titles.set(snapshot.title, [...(titles.get(snapshot.title) ?? []), route.path]);
590
- const place = routePlace(route);
634
+ const place = placeOfRoute(route);
591
635
  if (h1s.length === 1) {
592
- const problems = titleHeadingProblems(snapshot.title, h1s[0], place ? geography.placeNames.get(place) : undefined);
636
+ const problems = titleHeadingProblems(snapshot.title, h1s[0], place && !place.region ? geography.placeNames.get(place.slug) : undefined);
593
637
  if (problems.length > 0)
594
638
  add('title-h1', problems.join('; '));
595
639
  }
@@ -612,7 +656,7 @@ export function defineBrandlabPageQualityTests(options) {
612
656
  for (const failure of inspectStructuredData(route, snapshot.jsonLd, businessId)) {
613
657
  add('structured-data', failure.slice(route.path.length + 2));
614
658
  }
615
- const place = routePlace(route);
659
+ const place = placeOfRoute(route)?.slug;
616
660
  if (indexable && place)
617
661
  similarityPages.push({ path: route.path, place, blocks: textBlocks(html).map(geography.maskPlaces) });
618
662
  const targets = new Set();
@@ -673,19 +717,23 @@ export function defineBrandlabPageQualityTests(options) {
673
717
  findings.push({ path: item.path, kind: 'location-review', detail: item.reasons.join('; ') });
674
718
  findings.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
675
719
  const byKind = Object.fromEntries([...new Set(findings.map((finding) => finding.kind))].sort().map((kind) => [kind, findings.filter((finding) => finding.kind === kind).length]));
676
- await testInfo.attach('page-quality.json', {
677
- body: JSON.stringify({ pages: routes.length, findings: findings.length, byKind, details: findings }, null, 2),
678
- contentType: 'application/json',
679
- });
680
- await testInfo.attach('location-review.json', {
681
- body: JSON.stringify({
720
+ const reports = {
721
+ 'page-quality.json': JSON.stringify({ pages: routes.length, findings: findings.length, byKind, details: findings }, null, 2),
722
+ 'location-review.json': JSON.stringify({
682
723
  reviewed: review.length,
683
724
  note: 'Pages on the review list stay indexed. Each review ends in adding evidence, merging into the nearest owner or hiding with noindex, follow; merging and hiding are Romie\'s decision.',
684
725
  similarity: similarity.sort((a, b) => b.score - a.score),
685
726
  review,
686
727
  }, null, 2),
687
- contentType: 'application/json',
688
- });
728
+ };
729
+ for (const [name, body] of Object.entries(reports))
730
+ await testInfo.attach(name, { body, contentType: 'application/json' });
731
+ // CI's page-quality run keeps the reports beside the ready report (BRANDLAB_PAGE_QUALITY_DIR=docs/ready).
732
+ if (run.directory) {
733
+ mkdirSync(run.directory, { recursive: true });
734
+ for (const [name, body] of Object.entries(reports))
735
+ writeFileSync(join(run.directory, name), `${body}\n`);
736
+ }
689
737
  testInfo.annotations.push({
690
738
  type: 'advisory',
691
739
  description: `${findings.length} page-quality finding(s) across ${routes.length} page(s)${findings.length ? `: ${Object.entries(byKind).map(([kind, count]) => `${kind} ${count}`).join(', ')}` : ''}; see page-quality.json.`,
@@ -693,6 +741,18 @@ export function defineBrandlabPageQualityTests(options) {
693
741
  });
694
742
  });
695
743
  }
744
+ /**
745
+ * Whether the advisory page-quality crawl runs (3.13.0, client CI v2). CI v2 runs it on its own when a pull request
746
+ * is ready for review, on a manual run and monthly for large estates, and sets `BRANDLAB_PAGE_QUALITY=skip` for the required journeys so
747
+ * drafts and merges do not crawl every page twice. `BRANDLAB_PAGE_QUALITY_DIR` also writes its reports there.
748
+ */
749
+ export function pageQualityRunFromEnv(env = process.env) {
750
+ const directory = env.BRANDLAB_PAGE_QUALITY_DIR?.trim() || undefined;
751
+ if ((env.BRANDLAB_PAGE_QUALITY ?? '').trim() === 'skip') {
752
+ return { run: false, reason: 'The advisory page-quality crawl runs on its own in CI (ready for review, manual runs and monthly for large estates); BRANDLAB_PAGE_QUALITY=skip.' };
753
+ }
754
+ return { run: true, reason: '', ...(directory ? { directory } : {}) };
755
+ }
696
756
  function inspectStructuredData(route, payloads, businessId) {
697
757
  const failures = [];
698
758
  if (payloads.some((payload) => payload.parseError))