@brandlab-digital/site-core 2.1.0 → 3.0.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/dist/locations.js CHANGED
@@ -68,8 +68,13 @@ export function getCoverageGroupSuburbSlugs(coverageGroupSlugs, registry) {
68
68
  * are resolved before comparison so migrated repos that kept live URLs can
69
69
  * still pass. Returns an empty array when the subset is clean.
70
70
  */
71
- export function validateCatalogAgainstRegistry(catalog, registry) {
71
+ export function validateCatalogAgainstRegistry(catalog, registry, options = {}) {
72
72
  const issues = [];
73
+ if (options.policy)
74
+ assertPolicyMatchesRegistry(options.policy, registry);
75
+ const policyExclusions = options.policy ? getPolicyExcludedSlugs(options.policy) : undefined;
76
+ const retained = new Set(options.retainedExclusions ?? []);
77
+ const siteOwned = new Set(options.siteOwned ?? []);
73
78
  const regionSlugSet = new Set(registry.regions.map((region) => region.slug));
74
79
  const subRegionSlugSet = new Set(registry.subRegions.map((subRegion) => subRegion.slug));
75
80
  const coverageGroupSlugSet = new Set(registry.coverageGroups.map((group) => group.slug));
@@ -78,7 +83,7 @@ export function validateCatalogAgainstRegistry(catalog, registry) {
78
83
  const nonSuburbLocalityBySlug = new Map(registry.nonSuburbLocalities.map((locality) => [locality.slug, locality]));
79
84
  for (const rawRegionSlug of catalog.regionSlugs) {
80
85
  const regionSlug = resolveRegistryAlias(rawRegionSlug, registry, ['region', 'subRegion']);
81
- if (!regionSlugSet.has(regionSlug) && !subRegionSlugSet.has(regionSlug)) {
86
+ if (!regionSlugSet.has(regionSlug) && !subRegionSlugSet.has(regionSlug) && !siteOwned.has(rawRegionSlug)) {
82
87
  issues.push({
83
88
  kind: 'unknown-region',
84
89
  slug: rawRegionSlug,
@@ -101,7 +106,11 @@ export function validateCatalogAgainstRegistry(catalog, registry) {
101
106
  : resolveRegistryAlias(suburb.slug, registry, 'suburb');
102
107
  const registrySuburb = suburbBySlug.get(resolvedSuburbSlug);
103
108
  if (!registrySuburb) {
109
+ if (siteOwned.has(suburb.slug) && !nonSuburbLocalityBySlug.has(suburb.slug))
110
+ continue;
104
111
  const excludedLocality = nonSuburbLocalityBySlug.get(suburb.slug);
112
+ if (excludedLocality && options.policy && retained.has(suburb.slug))
113
+ continue;
105
114
  if (excludedLocality) {
106
115
  issues.push({
107
116
  kind: 'excluded-website-locality',
@@ -117,6 +126,13 @@ export function validateCatalogAgainstRegistry(catalog, registry) {
117
126
  });
118
127
  continue;
119
128
  }
129
+ if (policyExclusions?.has(registrySuburb.slug) && !retained.has(suburb.slug)) {
130
+ issues.push({
131
+ kind: 'excluded-by-policy',
132
+ slug: suburb.slug,
133
+ detail: `Suburb "${suburb.slug}" is excluded by website-location policy ${options.policy.version}: ${policyExclusions.get(registrySuburb.slug)}`,
134
+ });
135
+ }
120
136
  const regionSlug = resolveRegistryAlias(suburb.regionSlug, registry, ['region', 'subRegion']);
121
137
  const regionMatches = registrySuburb.regionSlug === regionSlug ||
122
138
  registry.subRegions.some((subRegion) => subRegion.slug === regionSlug &&
@@ -180,4 +196,276 @@ function getRegistrySlugSet(registry, kind) {
180
196
  return new Set(registry.suburbs.map((suburb) => suburb.slug));
181
197
  }
182
198
  }
199
+ function assertPolicyMatchesRegistry(policy, registry) {
200
+ if (!policy || typeof policy !== 'object' || policy.registryVersion !== registry.version) {
201
+ throw new Error(`Website-location policy ${policy?.version ?? 'unknown'} applies to registry ${policy?.registryVersion ?? 'unknown'}, not ${registry.version}.`);
202
+ }
203
+ }
204
+ /** Slug → reason for every suburb or locality the website-location policy excludes. */
205
+ function getPolicyExcludedSlugs(policy) {
206
+ const excluded = new Map();
207
+ for (const group of policy.defaultExcludedSuburbGroups) {
208
+ for (const slug of group.slugs)
209
+ excluded.set(slug, group.reason);
210
+ }
211
+ for (const [slug, locality] of Object.entries(policy.excludedLocalities)) {
212
+ excluded.set(slug, locality.reason);
213
+ }
214
+ return excluded;
215
+ }
216
+ const LOCATION_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
217
+ const estateLookups = new WeakMap();
218
+ /** Canonical registry entities by slug; suburbs win over regions, sub-regions and localities. */
219
+ function getEstateLookup(registry) {
220
+ let lookup = estateLookups.get(registry);
221
+ if (!lookup) {
222
+ const entries = new Map();
223
+ const add = (kind, records) => {
224
+ for (const record of records) {
225
+ if (!entries.has(record.slug)) {
226
+ entries.set(record.slug, { kind, registrySlug: record.slug, name: record.name });
227
+ }
228
+ }
229
+ };
230
+ add('suburb', registry.suburbs);
231
+ add('region', registry.regions);
232
+ add('subRegion', registry.subRegions);
233
+ add('locality', registry.nonSuburbLocalities);
234
+ lookup = entries;
235
+ estateLookups.set(registry, lookup);
236
+ }
237
+ return lookup;
238
+ }
239
+ function resolveEstateLocation(slug, registry) {
240
+ const lookup = getEstateLookup(registry);
241
+ const direct = lookup.get(slug);
242
+ if (direct)
243
+ return direct;
244
+ const target = registry.aliases[slug];
245
+ const aliased = target ? lookup.get(target) : undefined;
246
+ return aliased && aliased.kind !== 'locality' ? aliased : undefined;
247
+ }
248
+ /**
249
+ * The registry location or alias a site-owned slug would shadow. Councils and
250
+ * coverage groups are not location pages, so a site may own a page at one of
251
+ * their slugs.
252
+ */
253
+ function registryLocationAt(slug, registry) {
254
+ const location = getEstateLookup(registry).get(slug);
255
+ if (location)
256
+ return `the registry ${location.kind === 'subRegion' ? 'sub-region' : location.kind} "${location.name}"`;
257
+ const target = Object.hasOwn(registry.aliases, slug) ? registry.aliases[slug] : undefined;
258
+ return target ? `a registry alias of "${target}"` : undefined;
259
+ }
260
+ const SITE_OWNED_KEYS = new Set(['reason', 'slugs']);
261
+ /** Site-owned slug → reason, with every issue in the declaration. */
262
+ function readSiteOwned(siteOwned, registry, issues) {
263
+ const owned = new Map();
264
+ siteOwned.forEach((group, index) => {
265
+ const label = `siteOwned[${index}]`;
266
+ if (!group ||
267
+ typeof group !== 'object' ||
268
+ Array.isArray(group) ||
269
+ Object.keys(group).some((key) => !SITE_OWNED_KEYS.has(key))) {
270
+ issues.push({ kind: 'invalid-site-owned', slug: '', detail: `${label} must be { reason, slugs }.` });
271
+ return;
272
+ }
273
+ const reason = typeof group.reason === 'string' ? group.reason.trim() : '';
274
+ if (!reason || group.reason.length > 500) {
275
+ issues.push({
276
+ kind: 'invalid-site-owned',
277
+ slug: '',
278
+ detail: `${label} needs a concise reason (at most 500 characters) for publishing outside the registry.`,
279
+ });
280
+ return;
281
+ }
282
+ if (!Array.isArray(group.slugs) || group.slugs.length === 0) {
283
+ issues.push({ kind: 'invalid-site-owned', slug: '', detail: `${label} must list at least one slug.` });
284
+ return;
285
+ }
286
+ for (const slug of group.slugs) {
287
+ if (typeof slug !== 'string' || !LOCATION_SLUG_PATTERN.test(slug)) {
288
+ issues.push({
289
+ kind: 'invalid-site-owned',
290
+ slug: String(slug),
291
+ detail: `${label} slugs must be lowercase kebab-case.`,
292
+ });
293
+ continue;
294
+ }
295
+ if (owned.has(slug)) {
296
+ issues.push({ kind: 'duplicate-site-owned', slug, detail: `"${slug}" is declared site-owned more than once.` });
297
+ continue;
298
+ }
299
+ const shadowed = registryLocationAt(slug, registry);
300
+ if (shadowed) {
301
+ issues.push({
302
+ kind: 'site-owned-in-registry',
303
+ slug,
304
+ detail: `"${slug}" is ${shadowed} in registry ${registry.version}; publish the registry location instead of declaring it site-owned.`,
305
+ });
306
+ continue;
307
+ }
308
+ owned.set(slug, group.reason);
309
+ }
310
+ });
311
+ return owned;
312
+ }
313
+ /**
314
+ * Policy-aware estate validation. Returns every issue instead of throwing so
315
+ * conformance can report them alongside its own rules.
316
+ */
317
+ export function validateLocationEstate(definition) {
318
+ const issues = [];
319
+ if (!definition ||
320
+ typeof definition !== 'object' ||
321
+ !definition.registry ||
322
+ !definition.policy ||
323
+ !Array.isArray(definition.published) ||
324
+ !Array.isArray(definition.retainedExclusions) ||
325
+ (definition.siteOwned !== undefined && !Array.isArray(definition.siteOwned))) {
326
+ return [{
327
+ kind: 'invalid-estate',
328
+ slug: '',
329
+ detail: 'A location estate requires registry, policy, published and retainedExclusions; siteOwned, when given, is a list.',
330
+ }];
331
+ }
332
+ const { registry, policy } = definition;
333
+ try {
334
+ assertPolicyMatchesRegistry(policy, registry);
335
+ }
336
+ catch (error) {
337
+ return [{ kind: 'policy-registry-mismatch', slug: '', detail: error.message }];
338
+ }
339
+ const siteOwned = readSiteOwned(definition.siteOwned ?? [], registry, issues);
340
+ const excluded = getPolicyExcludedSlugs(policy);
341
+ const retained = new Map();
342
+ for (const exclusion of definition.retainedExclusions) {
343
+ const slug = exclusion?.slug;
344
+ if (typeof slug !== 'string' ||
345
+ typeof exclusion.reason !== 'string' ||
346
+ !exclusion.reason.trim() ||
347
+ exclusion.reason.length > 500) {
348
+ issues.push({
349
+ kind: 'invalid-retained-exclusion',
350
+ slug: String(slug ?? ''),
351
+ detail: 'A retained exclusion needs its slug and a concise reason.',
352
+ });
353
+ continue;
354
+ }
355
+ if (retained.has(slug)) {
356
+ issues.push({ kind: 'duplicate-retained-exclusion', slug, detail: `"${slug}" is declared more than once.` });
357
+ continue;
358
+ }
359
+ if (siteOwned.has(slug)) {
360
+ issues.push({
361
+ kind: 'retained-exclusion-site-owned',
362
+ slug,
363
+ detail: `"${slug}" is site-owned; retained exclusions apply only to registry locations the policy excludes.`,
364
+ });
365
+ continue;
366
+ }
367
+ retained.set(slug, exclusion);
368
+ }
369
+ const published = new Set();
370
+ for (const slug of definition.published) {
371
+ if (typeof slug !== 'string' || !LOCATION_SLUG_PATTERN.test(slug)) {
372
+ issues.push({ kind: 'invalid-slug', slug: String(slug), detail: 'Location slugs must be lowercase kebab-case.' });
373
+ continue;
374
+ }
375
+ if (published.has(slug)) {
376
+ issues.push({ kind: 'duplicate-slug', slug, detail: `"${slug}" is published more than once.` });
377
+ continue;
378
+ }
379
+ published.add(slug);
380
+ if (siteOwned.has(slug))
381
+ continue;
382
+ const location = resolveEstateLocation(slug, registry);
383
+ if (!location) {
384
+ issues.push({
385
+ kind: 'unknown-location',
386
+ slug,
387
+ detail: `"${slug}" is not a region, sub-region, suburb, locality or alias in registry ${registry.version}, and not declared in siteOwned.`,
388
+ });
389
+ continue;
390
+ }
391
+ const reason = excluded.get(location.registrySlug) ??
392
+ (location.kind === 'locality'
393
+ ? 'Official locality excluded from the default website location catalogue.'
394
+ : undefined);
395
+ if (reason && !retained.has(slug)) {
396
+ issues.push({
397
+ kind: 'excluded-by-policy',
398
+ slug,
399
+ detail: `"${slug}" is excluded by website-location policy ${policy.version} (${reason}) and is not declared in retainedExclusions.`,
400
+ });
401
+ }
402
+ if (!reason && retained.has(slug)) {
403
+ issues.push({
404
+ kind: 'retained-exclusion-not-excluded',
405
+ slug,
406
+ detail: `"${slug}" is not excluded by policy ${policy.version}; remove it from retainedExclusions.`,
407
+ });
408
+ }
409
+ }
410
+ for (const slug of retained.keys()) {
411
+ if (!published.has(slug)) {
412
+ issues.push({
413
+ kind: 'retained-exclusion-not-published',
414
+ slug,
415
+ detail: `"${slug}" is declared as retained but is not published.`,
416
+ });
417
+ }
418
+ }
419
+ for (const slug of siteOwned.keys()) {
420
+ if (!published.has(slug)) {
421
+ issues.push({
422
+ kind: 'site-owned-not-published',
423
+ slug,
424
+ detail: `"${slug}" is declared site-owned but is not published.`,
425
+ });
426
+ }
427
+ }
428
+ return issues;
429
+ }
430
+ /**
431
+ * Declares the published location estate (`src/data/locations/estate.ts`).
432
+ * Fails on unknown slugs and on policy-excluded slugs that are not declared
433
+ * as retained, so a registry or policy update can never silently add or drop
434
+ * a published page. Locations outside the registry are published only when a
435
+ * `siteOwned` group declares them with its reason.
436
+ */
437
+ export function defineLocationEstate(definition) {
438
+ const issues = validateLocationEstate(definition);
439
+ if (issues.length > 0) {
440
+ const shown = issues.slice(0, 20).map((entry) => `- ${entry.kind} ${entry.slug}: ${entry.detail}`);
441
+ const more = issues.length > shown.length ? `\n- …and ${issues.length - shown.length} more` : '';
442
+ throw new Error(`Invalid location estate:\n${shown.join('\n')}${more}`);
443
+ }
444
+ const retained = new Set(definition.retainedExclusions.map((exclusion) => exclusion.slug));
445
+ const siteOwned = Object.freeze((definition.siteOwned ?? []).map((group) => Object.freeze({ reason: group.reason, slugs: Object.freeze([...group.slugs]) })));
446
+ const ownedReason = new Map(siteOwned.flatMap((group) => group.slugs.map((slug) => [slug, group.reason])));
447
+ const entries = definition.published.map((slug) => {
448
+ const reason = ownedReason.get(slug);
449
+ if (reason !== undefined) {
450
+ return Object.freeze({ slug, kind: 'site-owned', retainedExclusion: false, siteOwned: true, reason });
451
+ }
452
+ return Object.freeze({
453
+ slug,
454
+ ...resolveEstateLocation(slug, definition.registry),
455
+ retainedExclusion: retained.has(slug),
456
+ siteOwned: false,
457
+ });
458
+ });
459
+ const bySlug = new Map(entries.map((entry) => [entry.slug, entry]));
460
+ return Object.freeze({
461
+ registryVersion: definition.registry.version,
462
+ policyVersion: definition.policy.version,
463
+ published: Object.freeze([...definition.published]),
464
+ retainedExclusions: Object.freeze(definition.retainedExclusions.map((exclusion) => Object.freeze({ ...exclusion }))),
465
+ siteOwned,
466
+ entries: Object.freeze(entries),
467
+ has: (slug) => bySlug.has(slug),
468
+ get: (slug) => bySlug.get(slug),
469
+ });
470
+ }
183
471
  //# sourceMappingURL=locations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"locations.js","sourceRoot":"","sources":["../src/locations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Location publication and geography-registry contracts.\n *\n * Brandlab owns route correctness and shared geography. Projects own their\n * content, evidence and publication decisions.\n */\n\n/**\n * The preferred location publication contract. A project owner explicitly\n * decides whether a route is indexable after checking customer usefulness,\n * geographic correctness and available evidence.\n */\nexport interface BrandlabLocationRouteDecision {\n indexing: 'index' | 'noindex-follow'\n rationale: string\n reviewedAt?: string\n}\n\nexport function defineLocationRouteDecision<const T extends BrandlabLocationRouteDecision>(\n input: T,\n): T {\n if (input.indexing !== 'index' && input.indexing !== 'noindex-follow') {\n throw new Error('Location route indexing must be \"index\" or \"noindex-follow\".')\n }\n if (!input.rationale.trim()) {\n throw new Error('Location route decision requires a concise rationale.')\n }\n if (input.reviewedAt && Number.isNaN(Date.parse(input.reviewedAt))) {\n throw new Error('Location route decision reviewedAt must be a valid date.')\n }\n return input\n}\n\nexport function resolveLocationRouteIndexing(\n decision: BrandlabLocationRouteDecision | null | undefined,\n): 'index' | 'noindex-follow' {\n if (!decision) {\n throw new Error('Location route decision is required.')\n }\n return defineLocationRouteDecision(decision).indexing\n}\n\n// ---------------------------------------------------------------------------\n// Geography registries (standards/registry/*-geography.json)\n// ---------------------------------------------------------------------------\n\nexport interface RegistryRegion {\n slug: string\n name: string\n /** Preferred query-facing region label for titles and primary headings. */\n seoName: string\n /** Recognised search/display variants; these never create additional URLs. */\n alternateNames?: readonly string[]\n /** 'the' → copy reads \"the Eastern Suburbs\"; 'none' → \"Western Sydney\". */\n articleForm: 'the' | 'none'\n councilSlugs: readonly string[]\n}\n\nexport interface RegistrySubRegion {\n slug: string\n name: string\n parentRegionSlug: string\n /** Council-combo sub-region membership. */\n councilSlugs?: readonly string[]\n /** Explicit suburb membership for district-style sub-regions. */\n suburbSlugs?: readonly string[]\n /** Provenance for promoted project districts or audited groupings. */\n source?: string\n}\n\n/**\n * Internal service-coverage partition. Coverage groups have no public name,\n * route, metadata, schema, sitemap, breadcrumb, navigation, or page model.\n */\nexport interface RegistryCoverageGroup {\n slug: string\n /** Maintainer-facing label; never render this value as customer copy. */\n adminLabel: string\n regionSlug: string\n visibility: 'internal'\n}\n\nexport interface RegistryCouncil {\n slug: string\n name: string\n}\n\nexport interface RegistrySuburb {\n name: string\n slug: string\n regionSlug: string\n /** Internal service-coverage partition; never a page parent or URL segment. */\n coverageGroupSlug?: string\n councilSlug: string\n /** Secondary council memberships for split localities. */\n sharedCouncilSlugs?: readonly string[]\n /** Australia Post-matched postcode. */\n postcode: string\n /** Stewardship notes for boundary bindings and corrections. */\n notes?: string\n}\n\nexport type RegistryNonSuburbEntityType =\n | 'rural-locality'\n | 'natural-area'\n | 'protected-area'\n | 'waterbody'\n\n/**\n * Official NSW geography retained for spatial and council completeness but\n * excluded from the normal website suburb catalog.\n */\nexport interface RegistryNonSuburbLocality {\n name: string\n slug: string\n regionSlug: string\n councilSlug: string\n sharedCouncilSlugs?: readonly string[]\n postcode: null\n entityType: RegistryNonSuburbEntityType\n officialUrbanity: 'R'\n defaultWebsiteEligibility: false\n eligibilityReason: string\n notes?: string\n}\n\nexport type RegistryOfficialLocality = RegistrySuburb | RegistryNonSuburbLocality\n\nexport interface WebsiteLocationPolicyExclusionGroup {\n reasonCode: string\n reason: string\n slugs: readonly string[]\n}\n\nexport interface WebsiteLocationPolicyExcludedLocality {\n entityType: RegistryNonSuburbEntityType\n officialUrbanity: 'R'\n reason: string\n}\n\n/**\n * Website eligibility policy layered over factual geography.\n *\n * The registry continues to retain every official locality. The policy keeps\n * owner-excluded places out of normal project location catalogs and records\n * how existing public URLs must be retired.\n */\nexport interface WebsiteLocationPolicy {\n version: string\n generated: string\n registryVersion: string\n scope: string\n defaultCandidateRule: string\n projectOverrideRule: string\n sourceSnapshot: string\n defaultExcludedSuburbGroups: readonly WebsiteLocationPolicyExclusionGroup[]\n excludedLocalities: Readonly<Record<string, WebsiteLocationPolicyExcludedLocality>>\n}\n\nexport interface GeographyRegistry {\n version: string\n generated: string\n /** Provenance: where the registry data was derived from. */\n sources?: readonly string[]\n /** Stewardship notes for maintainers. */\n notes?: string\n regions: readonly RegistryRegion[]\n subRegions: readonly RegistrySubRegion[]\n /** Backend-only groupings used to select served suburbs. */\n coverageGroups: readonly RegistryCoverageGroup[]\n /** Legacy slug → canonical slug. */\n aliases: Readonly<Record<string, string>>\n councils: readonly RegistryCouncil[]\n /** Postcode-backed candidates only; this does not itself approve pages. */\n suburbs: readonly RegistrySuburb[]\n /** Official non-suburb geography excluded from default website locations. */\n nonSuburbLocalities: readonly RegistryNonSuburbLocality[]\n}\n\nexport type RegistryEntityKind = 'region' | 'subRegion' | 'council' | 'suburb'\n\n/** Canonical Brandlab slugify — identical to the benchmark implementation. */\nexport function toLocationSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n}\n\n/**\n * Resolve a possibly-legacy slug within an explicit registry entity kind.\n *\n * Entity context is required because canonical suburb slugs such as\n * `blacktown`, `hornsby`, `randwick`, and `willoughby` are also legacy council\n * aliases. A canonical slug for the requested kind always wins over an alias.\n */\nexport function resolveRegistryAlias(\n slug: string,\n registry: GeographyRegistry,\n kind: RegistryEntityKind | readonly RegistryEntityKind[],\n): string {\n const kinds = Array.isArray(kind) ? kind : [kind]\n const canonicalSets = kinds.map((entityKind) => getRegistrySlugSet(registry, entityKind))\n\n if (canonicalSets.some((slugSet) => slugSet.has(slug))) return slug\n\n const target = registry.aliases[slug]\n if (!target) return slug\n\n return canonicalSets.some((slugSet) => slugSet.has(target)) ? target : slug\n}\n\nexport interface CatalogRegistryIssue {\n kind:\n | 'unknown-region'\n | 'unknown-coverage-group'\n | 'unknown-council'\n | 'unknown-suburb'\n | 'excluded-website-locality'\n | 'suburb-region-mismatch'\n | 'suburb-coverage-group-mismatch'\n | 'suburb-council-mismatch'\n slug: string\n detail: string\n}\n\nexport interface CatalogForValidation {\n regionSlugs: readonly string[]\n /** Backend coverage selectors; these are never treated as region/page slugs. */\n coverageGroupSlugs?: readonly string[]\n suburbs: readonly {\n slug: string\n regionSlug: string\n coverageGroupSlug?: string\n councilSlug?: string\n }[]\n}\n\n/**\n * Expand internal coverage selectors to canonical suburb slugs.\n *\n * This helper only answers where a business serves. It does not approve pages,\n * static params, sitemap entries, schema nodes, or internal links.\n */\nexport function getCoverageGroupSuburbSlugs(\n coverageGroupSlugs: readonly string[],\n registry: GeographyRegistry,\n): string[] {\n const knownCoverageGroupSlugs = new Set(registry.coverageGroups.map((group) => group.slug))\n const unknownCoverageGroupSlug = coverageGroupSlugs.find(\n (slug) => !knownCoverageGroupSlugs.has(slug),\n )\n if (unknownCoverageGroupSlug) {\n throw new Error(`Unknown registry coverage group \"${unknownCoverageGroupSlug}\".`)\n }\n\n const selected = new Set(coverageGroupSlugs)\n return registry.suburbs\n .filter(\n (suburb) => suburb.coverageGroupSlug && selected.has(suburb.coverageGroupSlug),\n )\n .map((suburb) => suburb.slug)\n}\n\n/**\n * Validate a project catalog subset against the shared registry. Alias slugs\n * are resolved before comparison so migrated repos that kept live URLs can\n * still pass. Returns an empty array when the subset is clean.\n */\nexport function validateCatalogAgainstRegistry(\n catalog: CatalogForValidation,\n registry: GeographyRegistry,\n): CatalogRegistryIssue[] {\n const issues: CatalogRegistryIssue[] = []\n const regionSlugSet = new Set(registry.regions.map((region) => region.slug))\n const subRegionSlugSet = new Set(registry.subRegions.map((subRegion) => subRegion.slug))\n const coverageGroupSlugSet = new Set(registry.coverageGroups.map((group) => group.slug))\n const councilSlugSet = new Set(registry.councils.map((council) => council.slug))\n const suburbBySlug = new Map(registry.suburbs.map((suburb) => [suburb.slug, suburb]))\n const nonSuburbLocalityBySlug = new Map(\n registry.nonSuburbLocalities.map((locality) => [locality.slug, locality]),\n )\n\n for (const rawRegionSlug of catalog.regionSlugs) {\n const regionSlug = resolveRegistryAlias(rawRegionSlug, registry, ['region', 'subRegion'])\n if (!regionSlugSet.has(regionSlug) && !subRegionSlugSet.has(regionSlug)) {\n issues.push({\n kind: 'unknown-region',\n slug: rawRegionSlug,\n detail: `Region slug \"${rawRegionSlug}\" is not a registry region, sub-region, or alias.`,\n })\n }\n }\n\n for (const coverageGroupSlug of catalog.coverageGroupSlugs ?? []) {\n if (!coverageGroupSlugSet.has(coverageGroupSlug)) {\n issues.push({\n kind: 'unknown-coverage-group',\n slug: coverageGroupSlug,\n detail: `Coverage group slug \"${coverageGroupSlug}\" is not an internal registry coverage group.`,\n })\n }\n }\n\n for (const suburb of catalog.suburbs) {\n const resolvedSuburbSlug = suburbBySlug.has(suburb.slug)\n ? suburb.slug\n : resolveRegistryAlias(suburb.slug, registry, 'suburb')\n const registrySuburb = suburbBySlug.get(resolvedSuburbSlug)\n\n if (!registrySuburb) {\n const excludedLocality = nonSuburbLocalityBySlug.get(suburb.slug)\n if (excludedLocality) {\n issues.push({\n kind: 'excluded-website-locality',\n slug: suburb.slug,\n detail: `Official locality \"${suburb.slug}\" is classified as ${excludedLocality.entityType} and excluded from the normal website suburb catalog: ${excludedLocality.eligibilityReason}`,\n })\n continue\n }\n issues.push({\n kind: 'unknown-suburb',\n slug: suburb.slug,\n detail: `Suburb slug \"${suburb.slug}\" is not in the registry (not gazetted, or needs adding to the registry first).`,\n })\n continue\n }\n\n const regionSlug = resolveRegistryAlias(suburb.regionSlug, registry, ['region', 'subRegion'])\n const regionMatches =\n registrySuburb.regionSlug === regionSlug ||\n registry.subRegions.some(\n (subRegion) =>\n subRegion.slug === regionSlug &&\n subRegion.parentRegionSlug === registrySuburb.regionSlug &&\n (subRegion.suburbSlugs?.includes(registrySuburb.slug) ||\n subRegion.councilSlugs?.some(\n (councilSlug) =>\n councilSlug === registrySuburb.councilSlug ||\n registrySuburb.sharedCouncilSlugs?.includes(councilSlug),\n )),\n )\n\n if (!regionMatches) {\n issues.push({\n kind: 'suburb-region-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to \"${suburb.regionSlug}\" but the registry places it in \"${registrySuburb.regionSlug}\".`,\n })\n }\n\n if (suburb.coverageGroupSlug) {\n if (!coverageGroupSlugSet.has(suburb.coverageGroupSlug)) {\n issues.push({\n kind: 'unknown-coverage-group',\n slug: suburb.coverageGroupSlug,\n detail: `Coverage group slug \"${suburb.coverageGroupSlug}\" is not an internal registry coverage group.`,\n })\n } else if (registrySuburb.coverageGroupSlug !== suburb.coverageGroupSlug) {\n issues.push({\n kind: 'suburb-coverage-group-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to coverage group \"${suburb.coverageGroupSlug}\" but the registry places it under \"${registrySuburb.coverageGroupSlug ?? 'none'}\".`,\n })\n }\n }\n\n if (suburb.councilSlug) {\n const councilSlug = resolveRegistryAlias(suburb.councilSlug, registry, 'council')\n if (!councilSlugSet.has(councilSlug)) {\n issues.push({\n kind: 'unknown-council',\n slug: suburb.councilSlug,\n detail: `Council slug \"${suburb.councilSlug}\" is not a registry council or alias.`,\n })\n } else if (\n registrySuburb.councilSlug !== councilSlug &&\n !registrySuburb.sharedCouncilSlugs?.includes(councilSlug)\n ) {\n issues.push({\n kind: 'suburb-council-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to council \"${suburb.councilSlug}\" but the registry places it under \"${registrySuburb.councilSlug}\".`,\n })\n }\n }\n }\n\n return issues\n}\n\nfunction getRegistrySlugSet(\n registry: GeographyRegistry,\n kind: RegistryEntityKind,\n): ReadonlySet<string> {\n switch (kind) {\n case 'region':\n return new Set(registry.regions.map((region) => region.slug))\n case 'subRegion':\n return new Set(registry.subRegions.map((subRegion) => subRegion.slug))\n case 'council':\n return new Set(registry.councils.map((council) => council.slug))\n case 'suburb':\n return new Set(registry.suburbs.map((suburb) => suburb.slug))\n }\n}\n"]}
1
+ {"version":3,"file":"locations.js","sourceRoot":"","sources":["../src/locations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,MAAM,UAAU,2BAA2B,CACzC,KAAQ;IAER,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,QAAQ,KAAK,gBAAgB,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAA;IACjF,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAA;IAC1E,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC7E,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,QAA0D;IAE1D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,2BAA2B,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAA;AACvD,CAAC;AA6ID,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI;SACR,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,QAA2B,EAC3B,IAAwD;IAExD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACjD,MAAM,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,kBAAkB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;IAEzF,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IAEnE,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7E,CAAC;AAsCD;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CACzC,kBAAqC,EACrC,QAA2B;IAE3B,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,MAAM,wBAAwB,GAAG,kBAAkB,CAAC,IAAI,CACtD,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,CAC7C,CAAA;IACD,IAAI,wBAAwB,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,oCAAoC,wBAAwB,IAAI,CAAC,CAAA;IACnF,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAC5C,OAAO,QAAQ,CAAC,OAAO;SACpB,MAAM,CACL,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,iBAAiB,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAC/E;SACA,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,8BAA8B,CAC5C,OAA6B,EAC7B,QAA2B,EAC3B,UAAoC,EAAE;IAEtC,MAAM,MAAM,GAA2B,EAAE,CAAA;IACzC,IAAI,OAAO,CAAC,MAAM;QAAE,2BAA2B,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACzE,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5F,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAA;IAC1D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAA;IAClD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IACxF,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACxF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAChF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IACrF,MAAM,uBAAuB,GAAG,IAAI,GAAG,CACrC,QAAQ,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAC1E,CAAA;IAED,KAAK,MAAM,aAAa,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QAChD,MAAM,UAAU,GAAG,oBAAoB,CAAC,aAAa,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAA;QACzF,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;YACzG,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,gBAAgB,aAAa,mDAAmD;aACzF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,IAAI,EAAE,EAAE,CAAC;QACjE,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,wBAAwB;gBAC9B,IAAI,EAAE,iBAAiB;gBACvB,MAAM,EAAE,wBAAwB,iBAAiB,+CAA+C;aACjG,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,kBAAkB,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;YACtD,CAAC,CAAC,MAAM,CAAC,IAAI;YACb,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;QACzD,MAAM,cAAc,GAAG,YAAY,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;QAE3D,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,SAAQ;YACrF,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACjE,IAAI,gBAAgB,IAAI,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,SAAQ;YAC7E,IAAI,gBAAgB,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,2BAA2B;oBACjC,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,MAAM,EAAE,sBAAsB,MAAM,CAAC,IAAI,sBAAsB,gBAAgB,CAAC,UAAU,yDAAyD,gBAAgB,CAAC,iBAAiB,EAAE;iBACxL,CAAC,CAAA;gBACF,SAAQ;YACV,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,gBAAgB,MAAM,CAAC,IAAI,iFAAiF;aACrH,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QAED,IAAI,gBAAgB,EAAE,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7E,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,WAAW,MAAM,CAAC,IAAI,4CAA4C,OAAO,CAAC,MAAO,CAAC,OAAO,KAAK,gBAAgB,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE;aAClJ,CAAC,CAAA;QACJ,CAAC;QAED,MAAM,UAAU,GAAG,oBAAoB,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAA;QAC7F,MAAM,aAAa,GACjB,cAAc,CAAC,UAAU,KAAK,UAAU;YACxC,QAAQ,CAAC,UAAU,CAAC,IAAI,CACtB,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,CAAC,IAAI,KAAK,UAAU;gBAC7B,SAAS,CAAC,gBAAgB,KAAK,cAAc,CAAC,UAAU;gBACxD,CAAC,SAAS,CAAC,WAAW,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC;oBACnD,SAAS,CAAC,YAAY,EAAE,IAAI,CAC1B,CAAC,WAAW,EAAE,EAAE,CACd,WAAW,KAAK,cAAc,CAAC,WAAW;wBAC1C,cAAc,CAAC,kBAAkB,EAAE,QAAQ,CAAC,WAAW,CAAC,CAC3D,CAAC,CACP,CAAA;QAEH,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,wBAAwB;gBAC9B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,WAAW,MAAM,CAAC,IAAI,qBAAqB,MAAM,CAAC,UAAU,oCAAoC,cAAc,CAAC,UAAU,IAAI;aACtI,CAAC,CAAA;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAC7B,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBACxD,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,wBAAwB;oBAC9B,IAAI,EAAE,MAAM,CAAC,iBAAiB;oBAC9B,MAAM,EAAE,wBAAwB,MAAM,CAAC,iBAAiB,+CAA+C;iBACxG,CAAC,CAAA;YACJ,CAAC;iBAAM,IAAI,cAAc,CAAC,iBAAiB,KAAK,MAAM,CAAC,iBAAiB,EAAE,CAAC;gBACzE,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,gCAAgC;oBACtC,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,MAAM,EAAE,WAAW,MAAM,CAAC,IAAI,oCAAoC,MAAM,CAAC,iBAAiB,uCAAuC,cAAc,CAAC,iBAAiB,IAAI,MAAM,IAAI;iBAChL,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,MAAM,WAAW,GAAG,oBAAoB,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAA;YACjF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBACrC,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,iBAAiB;oBACvB,IAAI,EAAE,MAAM,CAAC,WAAW;oBACxB,MAAM,EAAE,iBAAiB,MAAM,CAAC,WAAW,uCAAuC;iBACnF,CAAC,CAAA;YACJ,CAAC;iBAAM,IACL,cAAc,CAAC,WAAW,KAAK,WAAW;gBAC1C,CAAC,cAAc,CAAC,kBAAkB,EAAE,QAAQ,CAAC,WAAW,CAAC,EACzD,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,yBAAyB;oBAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,MAAM,EAAE,WAAW,MAAM,CAAC,IAAI,6BAA6B,MAAM,CAAC,WAAW,uCAAuC,cAAc,CAAC,WAAW,IAAI;iBACnJ,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,kBAAkB,CACzB,QAA2B,EAC3B,IAAwB;IAExB,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/D,KAAK,WAAW;YACd,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;QACxE,KAAK,SAAS;YACZ,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAClE,KAAK,QAAQ;YACX,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACjE,CAAC;AACH,CAAC;AAED,SAAS,2BAA2B,CAAC,MAA6B,EAAE,QAA2B;IAC7F,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,eAAe,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;QACzF,MAAM,IAAI,KAAK,CACb,2BAA2B,MAAM,EAAE,OAAO,IAAI,SAAS,wBAAwB,MAAM,EAAE,eAAe,IAAI,SAAS,SAAS,QAAQ,CAAC,OAAO,GAAG,CAChJ,CAAA;IACH,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,SAAS,sBAAsB,CAAC,MAA6B;IAC3D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,2BAA2B,EAAE,CAAC;QACvD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IAClE,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACzE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAoGD,MAAM,qBAAqB,GAAG,4BAA4B,CAAA;AAQ1D,MAAM,aAAa,GAAG,IAAI,OAAO,EAA0D,CAAA;AAE3F,iGAAiG;AACjG,SAAS,eAAe,CAAC,QAA2B;IAClD,IAAI,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACxC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAA;QACjD,MAAM,GAAG,GAAG,CAAC,IAAkC,EAAE,OAAkD,EAAE,EAAE;YACrG,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;gBAClF,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QACD,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC/B,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC/B,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAA;QACrC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAA;QAC7C,MAAM,GAAG,OAAO,CAAA;QAChB,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAY,EAAE,QAA2B;IACtE,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC/B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACvD,OAAO,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AACrE,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,IAAY,EAAE,QAA2B;IACnE,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACpD,IAAI,QAAQ;QAAE,OAAO,gBAAgB,QAAQ,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,GAAG,CAAA;IACtH,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzF,OAAO,MAAM,CAAC,CAAC,CAAC,wBAAwB,MAAM,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/D,CAAC;AAED,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;AAEpD,qEAAqE;AACrE,SAAS,aAAa,CACpB,SAAgD,EAChD,QAA2B,EAC3B,MAAqC;IAErC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACjC,MAAM,KAAK,GAAG,aAAa,KAAK,GAAG,CAAA;QACnC,IACE,CAAC,KAAK;YACN,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAC3D,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,KAAK,6BAA6B,EAAE,CAAC,CAAA;YACpG,OAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QAC1E,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACzC,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,GAAG,KAAK,uFAAuF;aACxG,CAAC,CAAA;YACF,OAAM;QACR,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,KAAK,+BAA+B,EAAE,CAAC,CAAA;YACtG,OAAM;QACR,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClE,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,oBAAoB;oBAC1B,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;oBAClB,MAAM,EAAE,GAAG,KAAK,sCAAsC;iBACvD,CAAC,CAAA;gBACF,SAAQ;YACV,CAAC;YACD,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,0CAA0C,EAAE,CAAC,CAAA;gBAC/G,SAAQ;YACV,CAAC;YACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;YACnD,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC;oBACV,IAAI,EAAE,wBAAwB;oBAC9B,IAAI;oBACJ,MAAM,EAAE,IAAI,IAAI,QAAQ,QAAQ,gBAAgB,QAAQ,CAAC,OAAO,qEAAqE;iBACtI,CAAC,CAAA;gBACF,SAAQ;YACV,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IACF,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAA4C;IAE5C,MAAM,MAAM,GAAkC,EAAE,CAAA;IAChD,IACE,CAAC,UAAU;QACX,OAAO,UAAU,KAAK,QAAQ;QAC9B,CAAC,UAAU,CAAC,QAAQ;QACpB,CAAC,UAAU,CAAC,MAAM;QAClB,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC;QACpC,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC;QAC7C,CAAC,UAAU,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EAC5E,CAAC;QACD,OAAO,CAAC;gBACN,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,EAAE;gBACR,MAAM,EAAE,kHAAkH;aAC3H,CAAC,CAAA;IACJ,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,UAAU,CAAA;IACvC,IAAI,CAAC;QACH,2BAA2B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAG,KAAe,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3F,CAAC;IACD,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC7E,MAAM,QAAQ,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAA;IAC/C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6C,CAAA;IACrE,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,kBAAkB,EAAE,CAAC;QACtD,MAAM,IAAI,GAAG,SAAS,EAAE,IAAI,CAAA;QAC5B,IACE,OAAO,IAAI,KAAK,QAAQ;YACxB,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ;YACpC,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE;YACxB,SAAS,CAAC,MAAM,CAAC,MAAM,GAAG,GAAG,EAC7B,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,4BAA4B;gBAClC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACxB,MAAM,EAAE,2DAA2D;aACpE,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,8BAA8B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,+BAA+B,EAAE,CAAC,CAAA;YAC5G,SAAQ;QACV,CAAC;QACD,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,+BAA+B;gBACrC,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,4FAA4F;aAC7G,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAC/B,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAA;IACnC,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;QACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,8CAA8C,EAAE,CAAC,CAAA;YACjH,SAAQ;QACV,CAAC;QACD,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,IAAI,gCAAgC,EAAE,CAAC,CAAA;YAC/F,SAAQ;QACV,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACnB,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAQ;QACjC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACtD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,kBAAkB;gBACxB,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,wEAAwE,QAAQ,CAAC,OAAO,kCAAkC;aAC3I,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GACV,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC;YACnC,CAAC,QAAQ,CAAC,IAAI,KAAK,UAAU;gBAC3B,CAAC,CAAC,yEAAyE;gBAC3E,CAAC,CAAC,SAAS,CAAC,CAAA;QAChB,IAAI,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,oBAAoB;gBAC1B,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,4CAA4C,MAAM,CAAC,OAAO,KAAK,MAAM,8CAA8C;aACpI,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,iCAAiC;gBACvC,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,+BAA+B,MAAM,CAAC,OAAO,sCAAsC;aACpG,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,kCAAkC;gBACxC,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,iDAAiD;aAClE,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,0BAA0B;gBAChC,IAAI;gBACJ,MAAM,EAAE,IAAI,IAAI,gDAAgD;aACjE,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAA4C;IAE5C,MAAM,MAAM,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAA;IACjD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;QAClG,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;QAChG,MAAM,IAAI,KAAK,CAAC,6BAA6B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAA;IACzE,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1F,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAC7B,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACzC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAChF,CACF,CAAA;IACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAU,CAAC,CAAC,CAAC,CAAA;IACnH,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAA+B,EAAE;QAC7E,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;QACvG,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,CAAC;YACnB,IAAI;YACJ,GAAG,qBAAqB,CAAC,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAE;YACpD,iBAAiB,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YACrC,SAAS,EAAE,KAAK;SACjB,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IACnE,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,eAAe,EAAE,UAAU,CAAC,QAAQ,CAAC,OAAO;QAC5C,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,OAAO;QACxC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;QACnD,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAC/B,UAAU,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC,CAClF;QACD,SAAS;QACT,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAC/B,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;QACvC,GAAG,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;KACxC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["/**\n * Location publication and geography-registry contracts.\n *\n * Brandlab owns route correctness and shared geography. Projects own their\n * content, evidence and publication decisions.\n */\n\n/**\n * The preferred location publication contract. A project owner explicitly\n * decides whether a route is indexable after checking customer usefulness,\n * geographic correctness and available evidence.\n */\nexport interface BrandlabLocationRouteDecision {\n indexing: 'index' | 'noindex-follow'\n rationale: string\n reviewedAt?: string\n}\n\nexport function defineLocationRouteDecision<const T extends BrandlabLocationRouteDecision>(\n input: T,\n): T {\n if (input.indexing !== 'index' && input.indexing !== 'noindex-follow') {\n throw new Error('Location route indexing must be \"index\" or \"noindex-follow\".')\n }\n if (!input.rationale.trim()) {\n throw new Error('Location route decision requires a concise rationale.')\n }\n if (input.reviewedAt && Number.isNaN(Date.parse(input.reviewedAt))) {\n throw new Error('Location route decision reviewedAt must be a valid date.')\n }\n return input\n}\n\nexport function resolveLocationRouteIndexing(\n decision: BrandlabLocationRouteDecision | null | undefined,\n): 'index' | 'noindex-follow' {\n if (!decision) {\n throw new Error('Location route decision is required.')\n }\n return defineLocationRouteDecision(decision).indexing\n}\n\n// ---------------------------------------------------------------------------\n// Geography registries (standards/registry/*-geography.json)\n// ---------------------------------------------------------------------------\n\nexport interface RegistryRegion {\n slug: string\n name: string\n /** Preferred query-facing region label for titles and primary headings. */\n seoName: string\n /** Recognised search/display variants; these never create additional URLs. */\n alternateNames?: readonly string[]\n /** 'the' → copy reads \"the Eastern Suburbs\"; 'none' → \"Western Sydney\". */\n articleForm: 'the' | 'none'\n councilSlugs: readonly string[]\n}\n\nexport interface RegistrySubRegion {\n slug: string\n name: string\n parentRegionSlug: string\n /** Council-combo sub-region membership. */\n councilSlugs?: readonly string[]\n /** Explicit suburb membership for district-style sub-regions. */\n suburbSlugs?: readonly string[]\n /** Provenance for promoted project districts or audited groupings. */\n source?: string\n}\n\n/**\n * Internal service-coverage partition. Coverage groups have no public name,\n * route, metadata, schema, sitemap, breadcrumb, navigation, or page model.\n */\nexport interface RegistryCoverageGroup {\n slug: string\n /** Maintainer-facing label; never render this value as customer copy. */\n adminLabel: string\n regionSlug: string\n visibility: 'internal'\n}\n\nexport interface RegistryCouncil {\n slug: string\n name: string\n}\n\nexport interface RegistrySuburb {\n name: string\n slug: string\n regionSlug: string\n /** Internal service-coverage partition; never a page parent or URL segment. */\n coverageGroupSlug?: string\n councilSlug: string\n /** Secondary council memberships for split localities. */\n sharedCouncilSlugs?: readonly string[]\n /** Australia Post-matched postcode. */\n postcode: string\n /** Stewardship notes for boundary bindings and corrections. */\n notes?: string\n}\n\nexport type RegistryNonSuburbEntityType =\n | 'rural-locality'\n | 'natural-area'\n | 'protected-area'\n | 'waterbody'\n\n/**\n * Official NSW geography retained for spatial and council completeness but\n * excluded from the normal website suburb catalog.\n */\nexport interface RegistryNonSuburbLocality {\n name: string\n slug: string\n regionSlug: string\n councilSlug: string\n sharedCouncilSlugs?: readonly string[]\n postcode: null\n entityType: RegistryNonSuburbEntityType\n officialUrbanity: 'R'\n defaultWebsiteEligibility: false\n eligibilityReason: string\n notes?: string\n}\n\nexport type RegistryOfficialLocality = RegistrySuburb | RegistryNonSuburbLocality\n\nexport interface WebsiteLocationPolicyExclusionGroup {\n reasonCode: string\n reason: string\n slugs: readonly string[]\n}\n\nexport interface WebsiteLocationPolicyExcludedLocality {\n entityType: RegistryNonSuburbEntityType\n officialUrbanity: 'R'\n reason: string\n}\n\n/**\n * Website eligibility policy layered over factual geography.\n *\n * The registry continues to retain every official locality. The policy keeps\n * owner-excluded places out of normal project location catalogs and records\n * how existing public URLs must be retired.\n */\nexport interface WebsiteLocationPolicy {\n version: string\n generated: string\n registryVersion: string\n scope: string\n defaultCandidateRule: string\n projectOverrideRule: string\n sourceSnapshot: string\n defaultExcludedSuburbGroups: readonly WebsiteLocationPolicyExclusionGroup[]\n excludedLocalities: Readonly<Record<string, WebsiteLocationPolicyExcludedLocality>>\n}\n\nexport interface GeographyRegistry {\n version: string\n generated: string\n /** Provenance: where the registry data was derived from. */\n sources?: readonly string[]\n /** Stewardship notes for maintainers. */\n notes?: string\n regions: readonly RegistryRegion[]\n subRegions: readonly RegistrySubRegion[]\n /** Backend-only groupings used to select served suburbs. */\n coverageGroups: readonly RegistryCoverageGroup[]\n /** Legacy slug → canonical slug. */\n aliases: Readonly<Record<string, string>>\n councils: readonly RegistryCouncil[]\n /** Postcode-backed candidates only; this does not itself approve pages. */\n suburbs: readonly RegistrySuburb[]\n /** Official non-suburb geography excluded from default website locations. */\n nonSuburbLocalities: readonly RegistryNonSuburbLocality[]\n}\n\nexport type RegistryEntityKind = 'region' | 'subRegion' | 'council' | 'suburb'\n\n/** Canonical Brandlab slugify — identical to the benchmark implementation. */\nexport function toLocationSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/(^-|-$)/g, '')\n}\n\n/**\n * Resolve a possibly-legacy slug within an explicit registry entity kind.\n *\n * Entity context is required because canonical suburb slugs such as\n * `blacktown`, `hornsby`, `randwick`, and `willoughby` are also legacy council\n * aliases. A canonical slug for the requested kind always wins over an alias.\n */\nexport function resolveRegistryAlias(\n slug: string,\n registry: GeographyRegistry,\n kind: RegistryEntityKind | readonly RegistryEntityKind[],\n): string {\n const kinds = Array.isArray(kind) ? kind : [kind]\n const canonicalSets = kinds.map((entityKind) => getRegistrySlugSet(registry, entityKind))\n\n if (canonicalSets.some((slugSet) => slugSet.has(slug))) return slug\n\n const target = registry.aliases[slug]\n if (!target) return slug\n\n return canonicalSets.some((slugSet) => slugSet.has(target)) ? target : slug\n}\n\nexport interface CatalogRegistryIssue {\n kind:\n | 'unknown-region'\n | 'unknown-coverage-group'\n | 'unknown-council'\n | 'unknown-suburb'\n | 'excluded-website-locality'\n | 'excluded-by-policy'\n | 'suburb-region-mismatch'\n | 'suburb-coverage-group-mismatch'\n | 'suburb-council-mismatch'\n slug: string\n detail: string\n}\n\nexport interface CatalogValidationOptions {\n /** Website-location policy layered over the registry; enables `excluded-by-policy`. */\n policy?: WebsiteLocationPolicy\n /** Policy-excluded slugs the project deliberately keeps live (see defineLocationEstate). */\n retainedExclusions?: readonly string[]\n /** Slugs the project publishes outside the registry (see defineLocationEstate's siteOwned). */\n siteOwned?: readonly string[]\n}\n\nexport interface CatalogForValidation {\n regionSlugs: readonly string[]\n /** Backend coverage selectors; these are never treated as region/page slugs. */\n coverageGroupSlugs?: readonly string[]\n suburbs: readonly {\n slug: string\n regionSlug: string\n coverageGroupSlug?: string\n councilSlug?: string\n }[]\n}\n\n/**\n * Expand internal coverage selectors to canonical suburb slugs.\n *\n * This helper only answers where a business serves. It does not approve pages,\n * static params, sitemap entries, schema nodes, or internal links.\n */\nexport function getCoverageGroupSuburbSlugs(\n coverageGroupSlugs: readonly string[],\n registry: GeographyRegistry,\n): string[] {\n const knownCoverageGroupSlugs = new Set(registry.coverageGroups.map((group) => group.slug))\n const unknownCoverageGroupSlug = coverageGroupSlugs.find(\n (slug) => !knownCoverageGroupSlugs.has(slug),\n )\n if (unknownCoverageGroupSlug) {\n throw new Error(`Unknown registry coverage group \"${unknownCoverageGroupSlug}\".`)\n }\n\n const selected = new Set(coverageGroupSlugs)\n return registry.suburbs\n .filter(\n (suburb) => suburb.coverageGroupSlug && selected.has(suburb.coverageGroupSlug),\n )\n .map((suburb) => suburb.slug)\n}\n\n/**\n * Validate a project catalog subset against the shared registry. Alias slugs\n * are resolved before comparison so migrated repos that kept live URLs can\n * still pass. Returns an empty array when the subset is clean.\n */\nexport function validateCatalogAgainstRegistry(\n catalog: CatalogForValidation,\n registry: GeographyRegistry,\n options: CatalogValidationOptions = {},\n): CatalogRegistryIssue[] {\n const issues: CatalogRegistryIssue[] = []\n if (options.policy) assertPolicyMatchesRegistry(options.policy, registry)\n const policyExclusions = options.policy ? getPolicyExcludedSlugs(options.policy) : undefined\n const retained = new Set(options.retainedExclusions ?? [])\n const siteOwned = new Set(options.siteOwned ?? [])\n const regionSlugSet = new Set(registry.regions.map((region) => region.slug))\n const subRegionSlugSet = new Set(registry.subRegions.map((subRegion) => subRegion.slug))\n const coverageGroupSlugSet = new Set(registry.coverageGroups.map((group) => group.slug))\n const councilSlugSet = new Set(registry.councils.map((council) => council.slug))\n const suburbBySlug = new Map(registry.suburbs.map((suburb) => [suburb.slug, suburb]))\n const nonSuburbLocalityBySlug = new Map(\n registry.nonSuburbLocalities.map((locality) => [locality.slug, locality]),\n )\n\n for (const rawRegionSlug of catalog.regionSlugs) {\n const regionSlug = resolveRegistryAlias(rawRegionSlug, registry, ['region', 'subRegion'])\n if (!regionSlugSet.has(regionSlug) && !subRegionSlugSet.has(regionSlug) && !siteOwned.has(rawRegionSlug)) {\n issues.push({\n kind: 'unknown-region',\n slug: rawRegionSlug,\n detail: `Region slug \"${rawRegionSlug}\" is not a registry region, sub-region, or alias.`,\n })\n }\n }\n\n for (const coverageGroupSlug of catalog.coverageGroupSlugs ?? []) {\n if (!coverageGroupSlugSet.has(coverageGroupSlug)) {\n issues.push({\n kind: 'unknown-coverage-group',\n slug: coverageGroupSlug,\n detail: `Coverage group slug \"${coverageGroupSlug}\" is not an internal registry coverage group.`,\n })\n }\n }\n\n for (const suburb of catalog.suburbs) {\n const resolvedSuburbSlug = suburbBySlug.has(suburb.slug)\n ? suburb.slug\n : resolveRegistryAlias(suburb.slug, registry, 'suburb')\n const registrySuburb = suburbBySlug.get(resolvedSuburbSlug)\n\n if (!registrySuburb) {\n if (siteOwned.has(suburb.slug) && !nonSuburbLocalityBySlug.has(suburb.slug)) continue\n const excludedLocality = nonSuburbLocalityBySlug.get(suburb.slug)\n if (excludedLocality && options.policy && retained.has(suburb.slug)) continue\n if (excludedLocality) {\n issues.push({\n kind: 'excluded-website-locality',\n slug: suburb.slug,\n detail: `Official locality \"${suburb.slug}\" is classified as ${excludedLocality.entityType} and excluded from the normal website suburb catalog: ${excludedLocality.eligibilityReason}`,\n })\n continue\n }\n issues.push({\n kind: 'unknown-suburb',\n slug: suburb.slug,\n detail: `Suburb slug \"${suburb.slug}\" is not in the registry (not gazetted, or needs adding to the registry first).`,\n })\n continue\n }\n\n if (policyExclusions?.has(registrySuburb.slug) && !retained.has(suburb.slug)) {\n issues.push({\n kind: 'excluded-by-policy',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is excluded by website-location policy ${options.policy!.version}: ${policyExclusions.get(registrySuburb.slug)}`,\n })\n }\n\n const regionSlug = resolveRegistryAlias(suburb.regionSlug, registry, ['region', 'subRegion'])\n const regionMatches =\n registrySuburb.regionSlug === regionSlug ||\n registry.subRegions.some(\n (subRegion) =>\n subRegion.slug === regionSlug &&\n subRegion.parentRegionSlug === registrySuburb.regionSlug &&\n (subRegion.suburbSlugs?.includes(registrySuburb.slug) ||\n subRegion.councilSlugs?.some(\n (councilSlug) =>\n councilSlug === registrySuburb.councilSlug ||\n registrySuburb.sharedCouncilSlugs?.includes(councilSlug),\n )),\n )\n\n if (!regionMatches) {\n issues.push({\n kind: 'suburb-region-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to \"${suburb.regionSlug}\" but the registry places it in \"${registrySuburb.regionSlug}\".`,\n })\n }\n\n if (suburb.coverageGroupSlug) {\n if (!coverageGroupSlugSet.has(suburb.coverageGroupSlug)) {\n issues.push({\n kind: 'unknown-coverage-group',\n slug: suburb.coverageGroupSlug,\n detail: `Coverage group slug \"${suburb.coverageGroupSlug}\" is not an internal registry coverage group.`,\n })\n } else if (registrySuburb.coverageGroupSlug !== suburb.coverageGroupSlug) {\n issues.push({\n kind: 'suburb-coverage-group-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to coverage group \"${suburb.coverageGroupSlug}\" but the registry places it under \"${registrySuburb.coverageGroupSlug ?? 'none'}\".`,\n })\n }\n }\n\n if (suburb.councilSlug) {\n const councilSlug = resolveRegistryAlias(suburb.councilSlug, registry, 'council')\n if (!councilSlugSet.has(councilSlug)) {\n issues.push({\n kind: 'unknown-council',\n slug: suburb.councilSlug,\n detail: `Council slug \"${suburb.councilSlug}\" is not a registry council or alias.`,\n })\n } else if (\n registrySuburb.councilSlug !== councilSlug &&\n !registrySuburb.sharedCouncilSlugs?.includes(councilSlug)\n ) {\n issues.push({\n kind: 'suburb-council-mismatch',\n slug: suburb.slug,\n detail: `Suburb \"${suburb.slug}\" is assigned to council \"${suburb.councilSlug}\" but the registry places it under \"${registrySuburb.councilSlug}\".`,\n })\n }\n }\n }\n\n return issues\n}\n\nfunction getRegistrySlugSet(\n registry: GeographyRegistry,\n kind: RegistryEntityKind,\n): ReadonlySet<string> {\n switch (kind) {\n case 'region':\n return new Set(registry.regions.map((region) => region.slug))\n case 'subRegion':\n return new Set(registry.subRegions.map((subRegion) => subRegion.slug))\n case 'council':\n return new Set(registry.councils.map((council) => council.slug))\n case 'suburb':\n return new Set(registry.suburbs.map((suburb) => suburb.slug))\n }\n}\n\nfunction assertPolicyMatchesRegistry(policy: WebsiteLocationPolicy, registry: GeographyRegistry) {\n if (!policy || typeof policy !== 'object' || policy.registryVersion !== registry.version) {\n throw new Error(\n `Website-location policy ${policy?.version ?? 'unknown'} applies to registry ${policy?.registryVersion ?? 'unknown'}, not ${registry.version}.`,\n )\n }\n}\n\n/** Slug → reason for every suburb or locality the website-location policy excludes. */\nfunction getPolicyExcludedSlugs(policy: WebsiteLocationPolicy): ReadonlyMap<string, string> {\n const excluded = new Map<string, string>()\n for (const group of policy.defaultExcludedSuburbGroups) {\n for (const slug of group.slugs) excluded.set(slug, group.reason)\n }\n for (const [slug, locality] of Object.entries(policy.excludedLocalities)) {\n excluded.set(slug, locality.reason)\n }\n return excluded\n}\n\n// ---------------------------------------------------------------------------\n// Published location estate (src/data/locations/estate.ts)\n// ---------------------------------------------------------------------------\n\nexport interface BrandlabRetainedLocationExclusion {\n slug: string\n /** Why this policy-excluded page stays live, for example a pending SEO decision. */\n reason: string\n}\n\n/**\n * Locations a site publishes outside the shared geography registry, for\n * example service areas beyond the registry's coverage. They skip registry\n * resolution and website-location policy, so each group states why.\n */\nexport interface BrandlabSiteOwnedLocations {\n /** Why these pages are published outside the registry (at most 500 characters). */\n reason: string\n /** Kebab-case slugs, each also listed in `published` and none a registry location or alias. */\n slugs: readonly string[]\n}\n\nexport interface BrandlabLocationEstateDefinition {\n registry: GeographyRegistry\n policy: WebsiteLocationPolicy\n /** Every published location slug, written as literal data; never derived from a registry list. */\n published: readonly string[]\n /** Published slugs the website-location policy excludes but the project keeps live (registry slugs only). */\n retainedExclusions: readonly BrandlabRetainedLocationExclusion[]\n /** Published slugs the site owns outside the registry, grouped by reason (default none). */\n siteOwned?: readonly BrandlabSiteOwnedLocations[]\n}\n\nexport type BrandlabRegistryLocationKind = 'region' | 'subRegion' | 'suburb' | 'locality'\n\nexport type BrandlabLocationEstateEntryKind = BrandlabRegistryLocationKind | 'site-owned'\n\nexport interface BrandlabRegistryLocationEstateEntry {\n slug: string\n kind: BrandlabRegistryLocationKind\n /** Registry slug the entry resolves to; differs from `slug` only for a legacy alias. */\n registrySlug: string\n name: string\n retainedExclusion: boolean\n siteOwned: false\n}\n\nexport interface BrandlabSiteOwnedLocationEstateEntry {\n slug: string\n kind: 'site-owned'\n /** Site-owned locations have no registry record; the site's own data names them. */\n registrySlug?: undefined\n name?: undefined\n retainedExclusion: false\n siteOwned: true\n /** The reason its siteOwned group declares. */\n reason: string\n}\n\nexport type BrandlabLocationEstateEntry =\n | BrandlabRegistryLocationEstateEntry\n | BrandlabSiteOwnedLocationEstateEntry\n\nexport interface BrandlabLocationEstate {\n registryVersion: string\n policyVersion: string\n published: readonly string[]\n retainedExclusions: readonly BrandlabRetainedLocationExclusion[]\n /** The site-owned groups as declared; `[]` when the site publishes registry locations only. */\n siteOwned: readonly BrandlabSiteOwnedLocations[]\n entries: readonly BrandlabLocationEstateEntry[]\n has(slug: string): boolean\n get(slug: string): BrandlabLocationEstateEntry | undefined\n}\n\nexport type BrandlabLocationEstateIssueKind =\n | 'invalid-estate'\n | 'policy-registry-mismatch'\n | 'invalid-slug'\n | 'duplicate-slug'\n | 'unknown-location'\n | 'excluded-by-policy'\n | 'invalid-retained-exclusion'\n | 'duplicate-retained-exclusion'\n | 'retained-exclusion-not-published'\n | 'retained-exclusion-not-excluded'\n | 'retained-exclusion-site-owned'\n | 'invalid-site-owned'\n | 'duplicate-site-owned'\n | 'site-owned-not-published'\n | 'site-owned-in-registry'\n\nexport interface BrandlabLocationEstateIssue {\n kind: BrandlabLocationEstateIssueKind\n slug: string\n detail: string\n}\n\nconst LOCATION_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n\ninterface EstateLocation {\n kind: BrandlabRegistryLocationKind\n registrySlug: string\n name: string\n}\n\nconst estateLookups = new WeakMap<GeographyRegistry, ReadonlyMap<string, EstateLocation>>()\n\n/** Canonical registry entities by slug; suburbs win over regions, sub-regions and localities. */\nfunction getEstateLookup(registry: GeographyRegistry) {\n let lookup = estateLookups.get(registry)\n if (!lookup) {\n const entries = new Map<string, EstateLocation>()\n const add = (kind: BrandlabRegistryLocationKind, records: readonly { slug: string; name: string }[]) => {\n for (const record of records) {\n if (!entries.has(record.slug)) {\n entries.set(record.slug, { kind, registrySlug: record.slug, name: record.name })\n }\n }\n }\n add('suburb', registry.suburbs)\n add('region', registry.regions)\n add('subRegion', registry.subRegions)\n add('locality', registry.nonSuburbLocalities)\n lookup = entries\n estateLookups.set(registry, lookup)\n }\n return lookup\n}\n\nfunction resolveEstateLocation(slug: string, registry: GeographyRegistry): EstateLocation | undefined {\n const lookup = getEstateLookup(registry)\n const direct = lookup.get(slug)\n if (direct) return direct\n const target = registry.aliases[slug]\n const aliased = target ? lookup.get(target) : undefined\n return aliased && aliased.kind !== 'locality' ? aliased : undefined\n}\n\n/**\n * The registry location or alias a site-owned slug would shadow. Councils and\n * coverage groups are not location pages, so a site may own a page at one of\n * their slugs.\n */\nfunction registryLocationAt(slug: string, registry: GeographyRegistry): string | undefined {\n const location = getEstateLookup(registry).get(slug)\n if (location) return `the registry ${location.kind === 'subRegion' ? 'sub-region' : location.kind} \"${location.name}\"`\n const target = Object.hasOwn(registry.aliases, slug) ? registry.aliases[slug] : undefined\n return target ? `a registry alias of \"${target}\"` : undefined\n}\n\nconst SITE_OWNED_KEYS = new Set(['reason', 'slugs'])\n\n/** Site-owned slug → reason, with every issue in the declaration. */\nfunction readSiteOwned(\n siteOwned: readonly BrandlabSiteOwnedLocations[],\n registry: GeographyRegistry,\n issues: BrandlabLocationEstateIssue[],\n): Map<string, string> {\n const owned = new Map<string, string>()\n siteOwned.forEach((group, index) => {\n const label = `siteOwned[${index}]`\n if (\n !group ||\n typeof group !== 'object' ||\n Array.isArray(group) ||\n Object.keys(group).some((key) => !SITE_OWNED_KEYS.has(key))\n ) {\n issues.push({ kind: 'invalid-site-owned', slug: '', detail: `${label} must be { reason, slugs }.` })\n return\n }\n const reason = typeof group.reason === 'string' ? group.reason.trim() : ''\n if (!reason || group.reason.length > 500) {\n issues.push({\n kind: 'invalid-site-owned',\n slug: '',\n detail: `${label} needs a concise reason (at most 500 characters) for publishing outside the registry.`,\n })\n return\n }\n if (!Array.isArray(group.slugs) || group.slugs.length === 0) {\n issues.push({ kind: 'invalid-site-owned', slug: '', detail: `${label} must list at least one slug.` })\n return\n }\n for (const slug of group.slugs) {\n if (typeof slug !== 'string' || !LOCATION_SLUG_PATTERN.test(slug)) {\n issues.push({\n kind: 'invalid-site-owned',\n slug: String(slug),\n detail: `${label} slugs must be lowercase kebab-case.`,\n })\n continue\n }\n if (owned.has(slug)) {\n issues.push({ kind: 'duplicate-site-owned', slug, detail: `\"${slug}\" is declared site-owned more than once.` })\n continue\n }\n const shadowed = registryLocationAt(slug, registry)\n if (shadowed) {\n issues.push({\n kind: 'site-owned-in-registry',\n slug,\n detail: `\"${slug}\" is ${shadowed} in registry ${registry.version}; publish the registry location instead of declaring it site-owned.`,\n })\n continue\n }\n owned.set(slug, group.reason)\n }\n })\n return owned\n}\n\n/**\n * Policy-aware estate validation. Returns every issue instead of throwing so\n * conformance can report them alongside its own rules.\n */\nexport function validateLocationEstate(\n definition: BrandlabLocationEstateDefinition,\n): BrandlabLocationEstateIssue[] {\n const issues: BrandlabLocationEstateIssue[] = []\n if (\n !definition ||\n typeof definition !== 'object' ||\n !definition.registry ||\n !definition.policy ||\n !Array.isArray(definition.published) ||\n !Array.isArray(definition.retainedExclusions) ||\n (definition.siteOwned !== undefined && !Array.isArray(definition.siteOwned))\n ) {\n return [{\n kind: 'invalid-estate',\n slug: '',\n detail: 'A location estate requires registry, policy, published and retainedExclusions; siteOwned, when given, is a list.',\n }]\n }\n const { registry, policy } = definition\n try {\n assertPolicyMatchesRegistry(policy, registry)\n } catch (error) {\n return [{ kind: 'policy-registry-mismatch', slug: '', detail: (error as Error).message }]\n }\n const siteOwned = readSiteOwned(definition.siteOwned ?? [], registry, issues)\n const excluded = getPolicyExcludedSlugs(policy)\n const retained = new Map<string, BrandlabRetainedLocationExclusion>()\n for (const exclusion of definition.retainedExclusions) {\n const slug = exclusion?.slug\n if (\n typeof slug !== 'string' ||\n typeof exclusion.reason !== 'string' ||\n !exclusion.reason.trim() ||\n exclusion.reason.length > 500\n ) {\n issues.push({\n kind: 'invalid-retained-exclusion',\n slug: String(slug ?? ''),\n detail: 'A retained exclusion needs its slug and a concise reason.',\n })\n continue\n }\n if (retained.has(slug)) {\n issues.push({ kind: 'duplicate-retained-exclusion', slug, detail: `\"${slug}\" is declared more than once.` })\n continue\n }\n if (siteOwned.has(slug)) {\n issues.push({\n kind: 'retained-exclusion-site-owned',\n slug,\n detail: `\"${slug}\" is site-owned; retained exclusions apply only to registry locations the policy excludes.`,\n })\n continue\n }\n retained.set(slug, exclusion)\n }\n\n const published = new Set<string>()\n for (const slug of definition.published) {\n if (typeof slug !== 'string' || !LOCATION_SLUG_PATTERN.test(slug)) {\n issues.push({ kind: 'invalid-slug', slug: String(slug), detail: 'Location slugs must be lowercase kebab-case.' })\n continue\n }\n if (published.has(slug)) {\n issues.push({ kind: 'duplicate-slug', slug, detail: `\"${slug}\" is published more than once.` })\n continue\n }\n published.add(slug)\n if (siteOwned.has(slug)) continue\n const location = resolveEstateLocation(slug, registry)\n if (!location) {\n issues.push({\n kind: 'unknown-location',\n slug,\n detail: `\"${slug}\" is not a region, sub-region, suburb, locality or alias in registry ${registry.version}, and not declared in siteOwned.`,\n })\n continue\n }\n const reason =\n excluded.get(location.registrySlug) ??\n (location.kind === 'locality'\n ? 'Official locality excluded from the default website location catalogue.'\n : undefined)\n if (reason && !retained.has(slug)) {\n issues.push({\n kind: 'excluded-by-policy',\n slug,\n detail: `\"${slug}\" is excluded by website-location policy ${policy.version} (${reason}) and is not declared in retainedExclusions.`,\n })\n }\n if (!reason && retained.has(slug)) {\n issues.push({\n kind: 'retained-exclusion-not-excluded',\n slug,\n detail: `\"${slug}\" is not excluded by policy ${policy.version}; remove it from retainedExclusions.`,\n })\n }\n }\n for (const slug of retained.keys()) {\n if (!published.has(slug)) {\n issues.push({\n kind: 'retained-exclusion-not-published',\n slug,\n detail: `\"${slug}\" is declared as retained but is not published.`,\n })\n }\n }\n for (const slug of siteOwned.keys()) {\n if (!published.has(slug)) {\n issues.push({\n kind: 'site-owned-not-published',\n slug,\n detail: `\"${slug}\" is declared site-owned but is not published.`,\n })\n }\n }\n return issues\n}\n\n/**\n * Declares the published location estate (`src/data/locations/estate.ts`).\n * Fails on unknown slugs and on policy-excluded slugs that are not declared\n * as retained, so a registry or policy update can never silently add or drop\n * a published page. Locations outside the registry are published only when a\n * `siteOwned` group declares them with its reason.\n */\nexport function defineLocationEstate(\n definition: BrandlabLocationEstateDefinition,\n): BrandlabLocationEstate {\n const issues = validateLocationEstate(definition)\n if (issues.length > 0) {\n const shown = issues.slice(0, 20).map((entry) => `- ${entry.kind} ${entry.slug}: ${entry.detail}`)\n const more = issues.length > shown.length ? `\\n- …and ${issues.length - shown.length} more` : ''\n throw new Error(`Invalid location estate:\\n${shown.join('\\n')}${more}`)\n }\n const retained = new Set(definition.retainedExclusions.map((exclusion) => exclusion.slug))\n const siteOwned = Object.freeze(\n (definition.siteOwned ?? []).map((group) =>\n Object.freeze({ reason: group.reason, slugs: Object.freeze([...group.slugs]) }),\n ),\n )\n const ownedReason = new Map(siteOwned.flatMap((group) => group.slugs.map((slug) => [slug, group.reason] as const)))\n const entries = definition.published.map((slug): BrandlabLocationEstateEntry => {\n const reason = ownedReason.get(slug)\n if (reason !== undefined) {\n return Object.freeze({ slug, kind: 'site-owned', retainedExclusion: false, siteOwned: true, reason })\n }\n return Object.freeze({\n slug,\n ...resolveEstateLocation(slug, definition.registry)!,\n retainedExclusion: retained.has(slug),\n siteOwned: false,\n })\n })\n const bySlug = new Map(entries.map((entry) => [entry.slug, entry]))\n return Object.freeze({\n registryVersion: definition.registry.version,\n policyVersion: definition.policy.version,\n published: Object.freeze([...definition.published]),\n retainedExclusions: Object.freeze(\n definition.retainedExclusions.map((exclusion) => Object.freeze({ ...exclusion })),\n ),\n siteOwned,\n entries: Object.freeze(entries),\n has: (slug: string) => bySlug.has(slug),\n get: (slug: string) => bySlug.get(slug),\n })\n}\n"]}
@@ -1,5 +1,11 @@
1
1
  import { type BrandlabIndexingDirective } from './routes.js';
2
2
  export type BrandlabMetadataTitleMode = 'templated' | 'absolute';
3
+ /**
4
+ * Meta keywords as production sites pass them: one comma-separated string, which
5
+ * renders exactly as given (`'a, b, c'` renders `content="a, b, c"`), or a list,
6
+ * which Next joins with commas (`['a', 'b', 'c']` renders `content="a,b,c"`).
7
+ */
8
+ export type BrandlabMetadataKeywords = string | readonly string[];
3
9
  export interface BrandlabSocialImage {
4
10
  url: string;
5
11
  twitterUrl?: string;
@@ -7,7 +13,7 @@ export interface BrandlabSocialImage {
7
13
  height?: number;
8
14
  alt?: string;
9
15
  }
10
- export interface BuildSeoMetadataInput {
16
+ export interface BuildBrandlabMetadataInput {
11
17
  brandName: string;
12
18
  siteUrl: string;
13
19
  locale: string;
@@ -18,7 +24,7 @@ export interface BuildSeoMetadataInput {
18
24
  pathname: string;
19
25
  titleMode?: BrandlabMetadataTitleMode;
20
26
  indexing: BrandlabIndexingDirective;
21
- keywords?: string[];
27
+ keywords?: BrandlabMetadataKeywords;
22
28
  article?: {
23
29
  publishedTime?: string;
24
30
  modifiedTime?: string;
@@ -26,7 +32,6 @@ export interface BuildSeoMetadataInput {
26
32
  };
27
33
  socialImage?: BrandlabSocialImage;
28
34
  }
29
- export type BuildBrandlabMetadataInput = BuildSeoMetadataInput;
30
35
  export interface BrandlabSeoMetadata {
31
36
  title: string | {
32
37
  absolute: string;
@@ -38,7 +43,8 @@ export interface BrandlabSeoMetadata {
38
43
  alternates: {
39
44
  canonical: string;
40
45
  };
41
- keywords?: string[];
46
+ /** Handed to Next unchanged in form: a string stays a string, a list stays a list. */
47
+ keywords?: string | string[];
42
48
  robots?: {
43
49
  index: boolean;
44
50
  follow: boolean;
@@ -66,6 +72,13 @@ export interface BrandlabSeoMetadata {
66
72
  images: string[];
67
73
  };
68
74
  }
75
+ export declare function assertBrandlabSocialImage(image: BrandlabSocialImage, field: string): void;
76
+ /**
77
+ * Validate keywords in either form without changing them. A string is one trimmed,
78
+ * single-line value whose comma-separated entries follow the list rules: at most 20
79
+ * distinct (case-insensitive) values of up to 100 characters.
80
+ */
81
+ export declare function assertBrandlabKeywords(keywords: unknown, field?: string): void;
69
82
  export declare function normalizeMetadataTitle(title: string, options: {
70
83
  brandName: string;
71
84
  titleMode?: BrandlabMetadataTitleMode;
@@ -73,6 +86,6 @@ export declare function normalizeMetadataTitle(title: string, options: {
73
86
  export declare function applyTitleTemplate(title: string, template: string): string;
74
87
  export declare function normalizeCanonicalPath(pathname: string): string;
75
88
  export declare function getAbsoluteUrl(siteUrl: string, pathname: string): string;
76
- export declare function buildSeoMetadata(input: BuildSeoMetadataInput): BrandlabSeoMetadata;
89
+ /** The one metadata builder: validates every input, then resolves canonical, robots and social output. */
77
90
  export declare function buildBrandlabMetadata(input: BuildBrandlabMetadataInput): BrandlabSeoMetadata;
78
91
  //# sourceMappingURL=metadata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}