@brandlab-digital/conformance 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +68 -2
  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/engineering.d.ts.map +1 -1
  12. package/dist/engineering.js +18 -2
  13. package/dist/engineering.js.map +1 -1
  14. package/dist/estate-snapshot.d.ts +54 -0
  15. package/dist/estate-snapshot.d.ts.map +1 -0
  16. package/dist/estate-snapshot.js +164 -0
  17. package/dist/estate-snapshot.js.map +1 -0
  18. package/dist/estate.d.ts +38 -0
  19. package/dist/estate.d.ts.map +1 -1
  20. package/dist/estate.js +71 -28
  21. package/dist/estate.js.map +1 -1
  22. package/dist/media-snapshot.d.ts +53 -0
  23. package/dist/media-snapshot.d.ts.map +1 -0
  24. package/dist/media-snapshot.js +66 -0
  25. package/dist/media-snapshot.js.map +1 -0
  26. package/dist/project.d.ts +20 -0
  27. package/dist/project.d.ts.map +1 -1
  28. package/dist/project.js +92 -2
  29. package/dist/project.js.map +1 -1
  30. package/dist/ready-cli.d.ts.map +1 -1
  31. package/dist/ready-cli.js +8 -3
  32. package/dist/ready-cli.js.map +1 -1
  33. package/dist/rule-registry.d.ts +5 -0
  34. package/dist/rule-registry.d.ts.map +1 -1
  35. package/dist/rule-registry.js +1 -0
  36. package/dist/rule-registry.js.map +1 -1
  37. package/dist/site-system/checks.d.ts +28 -3
  38. package/dist/site-system/checks.d.ts.map +1 -1
  39. package/dist/site-system/checks.js +5 -2
  40. package/dist/site-system/checks.js.map +1 -1
  41. package/dist/site-system/config.d.ts +13 -0
  42. package/dist/site-system/config.d.ts.map +1 -1
  43. package/dist/site-system/config.js +30 -1
  44. package/dist/site-system/config.js.map +1 -1
  45. package/dist/site-system/index.d.ts +1 -1
  46. package/dist/site-system/index.d.ts.map +1 -1
  47. package/dist/site-system/index.js +1 -1
  48. package/dist/site-system/index.js.map +1 -1
  49. package/dist/site-system/journeys/conversion.d.ts.map +1 -1
  50. package/dist/site-system/journeys/conversion.js +15 -2
  51. package/dist/site-system/journeys/conversion.js.map +1 -1
  52. package/dist/site-system/journeys/menu.d.ts.map +1 -1
  53. package/dist/site-system/journeys/menu.js +24 -1
  54. package/dist/site-system/journeys/menu.js.map +1 -1
  55. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
  56. package/dist/site-system/journeys/page-helpers.js +1 -1
  57. package/dist/site-system/journeys/page-helpers.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +13 -3
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +12 -2
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/shell.d.ts +1 -1
  63. package/dist/site-system/journeys/shell.js +2 -2
  64. package/dist/site-system/journeys/shell.js.map +1 -1
  65. package/dist/site-system/report.d.ts.map +1 -1
  66. package/dist/site-system/report.js +42 -8
  67. package/dist/site-system/report.js.map +1 -1
  68. package/dist/site-system/rule-modules.d.ts.map +1 -1
  69. package/dist/site-system/rule-modules.js +5 -0
  70. package/dist/site-system/rule-modules.js.map +1 -1
  71. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  72. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  73. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  74. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  75. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  76. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  77. package/dist/site-system/rules/delivery/index.js +7 -0
  78. package/dist/site-system/rules/delivery/index.js.map +1 -0
  79. package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
  80. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
  81. package/dist/site-system/rules/form/c7-thank-you.js +26 -3
  82. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
  83. package/dist/site-system/rules/form/index.d.ts.map +1 -1
  84. package/dist/site-system/rules/form/index.js +11 -1
  85. package/dist/site-system/rules/form/index.js.map +1 -1
  86. package/dist/site-system/rules/full/common.d.ts +2 -0
  87. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  88. package/dist/site-system/rules/full/common.js.map +1 -1
  89. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  90. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  91. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  92. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  93. package/dist/site-system/rules/full/estate.d.ts +26 -3
  94. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  95. package/dist/site-system/rules/full/estate.js +58 -9
  96. package/dist/site-system/rules/full/estate.js.map +1 -1
  97. package/dist/site-system/rules/full/geography.d.ts +15 -2
  98. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  99. package/dist/site-system/rules/full/geography.js +15 -2
  100. package/dist/site-system/rules/full/geography.js.map +1 -1
  101. package/dist/site-system/rules/full/index.d.ts +4 -3
  102. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  103. package/dist/site-system/rules/full/index.js +20 -5
  104. package/dist/site-system/rules/full/index.js.map +1 -1
  105. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  106. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  107. package/dist/site-system/rules/full/k12-slots.js +40 -0
  108. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  109. package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
  110. package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
  111. package/dist/site-system/rules/full/l10-location-copy.js +5 -2
  112. package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
  113. package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
  114. package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
  115. package/dist/site-system/rules/full/l3-neighbours.js +4 -1
  116. package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
  117. package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
  118. package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
  119. package/dist/site-system/rules/full/l4-local-copy.js +8 -3
  120. package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
  121. package/dist/site-system/rules/full/l9-data-contract.js +2 -2
  122. package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
  123. package/dist/site-system/rules/full/links.d.ts +3 -1
  124. package/dist/site-system/rules/full/links.d.ts.map +1 -1
  125. package/dist/site-system/rules/full/links.js +15 -5
  126. package/dist/site-system/rules/full/links.js.map +1 -1
  127. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  128. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  129. package/dist/site-system/rules/full/loc-index.js +43 -2
  130. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  131. package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
  132. package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
  133. package/dist/site-system/rules/full/route-kinds.js +52 -0
  134. package/dist/site-system/rules/full/route-kinds.js.map +1 -0
  135. package/dist/site-system/rules/full/strategy.d.ts +6 -0
  136. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  137. package/dist/site-system/rules/full/strategy.js +98 -10
  138. package/dist/site-system/rules/full/strategy.js.map +1 -1
  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 +13 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
  144. package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
  145. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
  146. package/dist/site-system/runners.js +1 -1
  147. package/dist/site-system/runners.js.map +1 -1
  148. package/dist/site-system-e2e.d.ts +1 -1
  149. package/dist/site-system-e2e.d.ts.map +1 -1
  150. package/dist/site-system-e2e.js +17 -4
  151. package/dist/site-system-e2e.js.map +1 -1
  152. package/package.json +10 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The full-tier rules (release 3.12.0): page families, location estates, section manifests, links and the local data\n * contract. They read the production build's route catalogue and pages (`.next`), the project's strategy record and\n * source, and the site's installed site-core (geography and section orders), and return findings for the ready report:\n * K12 (families), E5, E10, L3, L4, L6, L7, L8, L9, L10 (location), S4, the location indexing guard and, from 3.13.0,\n * W3 (estate refresh). The runner has the signature the ready gate registers.\n */\nimport { join } from \"node:path\";\nimport { readSiteSystemProjectConfig, type SiteSystemRefreshException } from \"../../config.js\";\nimport type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { type FullContext } from \"./common.js\";\nimport { checkE10 } from \"./e10-phrases.js\";\nimport { readEstate } from \"./estate.js\";\nimport { loadFullGeography, type FullGeography } from \"./geography.js\";\nimport { checkK12 } from \"./k12-manifests.js\";\nimport { checkL3 } from \"./l3-neighbours.js\";\nimport { checkL4 } from \"./l4-local-copy.js\";\nimport { checkL9 } from \"./l9-data-contract.js\";\nimport { checkL10 } from \"./l10-location-copy.js\";\nimport { checkE5, checkL6, checkL7, checkL8, inboundLinks } from \"./links.js\";\nimport { checkLocationIndexing, sydneyDay } from \"./loc-index.js\";\nimport { checkS4 } from \"./s4-route-files.js\";\nimport { readFullStrategy } from \"./strategy.js\";\nimport { checkW3 } from \"./w3-refresh.js\";\n\nexport { FULL_LISTED_FAILURES, type FullContext } from \"./common.js\";\nexport { headingPhrase } from \"./e10-phrases.js\";\nexport { readEstate, type Estate, type EstatePage, type LocationCopy, type ManifestData } from \"./estate.js\";\nexport { defineFullGeography, loadFullGeography, placeOfPath, type FullGeography, type FullGeographyInput } from \"./geography.js\";\nexport { K12_FAMILIES, isDataBackedReason } from \"./k12-manifests.js\";\nexport { L3_MAX_LIST, L3_MIN_LIST, L3_TOLERANCE_KM } from \"./l3-neighbours.js\";\nexport { L4_MIN_ROTATION_PAGES, L4_MIN_TEMPLATE_WORDS } from \"./l4-local-copy.js\";\nexport { yearStamp } from \"./l10-location-copy.js\";\nexport { L6_MIN_INBOUND, L6_MIN_PAGE_TYPES, L7_MAX_DEPTH, clickDepths } from \"./links.js\";\nexport { sydneyDay } from \"./loc-index.js\";\nexport { W3_FAMILIES, W3_MIN_REVALIDATE_SECONDS, refreshInterval } from \"./w3-refresh.js\";\nexport { FULL_STRATEGY_FILE, WAVE_MAX_GAP_DAYS, WAVE_MIN_GAP_DAYS, matchesWaveRoute, normalisePhrase, readFullStrategy, waveScheduleProblems } from \"./strategy.js\";\n\n/** The full-tier checks this module runs, in the order their findings are returned. */\nexport const FULL_TIER_CHECK_IDS = [\"K12\", \"E5\", \"E10\", \"L3\", \"L4\", \"L6\", \"L7\", \"L8\", \"L9\", \"L10\", \"S4\", \"LOC-INDEX\", \"W3\"] as const satisfies readonly SiteSystemCheckId[];\n\nexport interface FullTierRuleOptions {\n /** The day waves are judged on. Defaults to now. */\n readonly asOf?: Date;\n /** The build directory; defaults to `input.buildDir`, then `<projectRoot>/.next`. */\n readonly buildDir?: string;\n /** Places for a test; a real run loads the site's site-core geography. Null runs without places. */\n readonly geography?: FullGeography | null;\n /** Canonical section ids per family for a test; a real run loads site-core's `defaultSectionOrder`. */\n readonly defaultOrder?: FullContext[\"defaultOrder\"];\n}\n\n// Variable specifiers, so the package compiles without site-core; they resolve to the site's installed copy.\nconst SECTION_ORDER_MODULE: string = \"@brandlab-digital/site-core/section-order\";\n\nasync function loadDefaultOrder(): Promise<FullContext[\"defaultOrder\"]> {\n try {\n const module = (await import(SECTION_ORDER_MODULE)) as {\n defaultSectionOrder?: (family: string, trade?: string) => { cap: number; entries: readonly { id: string; requirement: string }[] };\n };\n const defaults = module.defaultSectionOrder;\n if (typeof defaults !== \"function\") return \"the installed site-core has no defaultSectionOrder (3.11.0 or later is needed)\";\n return (family, trade) => {\n try {\n const order = defaults(family, trade);\n return { cap: order.cap, entries: order.entries.map((entry) => ({ id: entry.id, requirement: entry.requirement })) };\n } catch {\n return undefined;\n }\n };\n } catch (error) {\n return `site-core's section orders could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n}\n\n/** The declared tier, the representative service route and the W3 exceptions; null tier when the config cannot be read in full. */\nfunction projectSettings(projectRoot: string): { tier: string | null; service: string | undefined; refreshExceptions: readonly SiteSystemRefreshException[] } {\n try {\n const config = readSiteSystemProjectConfig(projectRoot);\n // A config the gate cannot read in full could hide a full tier, so the checks run.\n return {\n tier: config.complete ? (config.siteSystem?.tier ?? \"none\") : null,\n service: config.representativeRoutes[\"service\"],\n refreshExceptions: config.siteSystem?.refreshExceptions ?? [],\n };\n } catch {\n return { tier: null, service: undefined, refreshExceptions: [] };\n }\n}\n\n/**\n * Runs the full-tier rules over the built estate at `projectRoot`. A site that declares a lower tier (or none) gets no\n * findings: the report shows the full checks as not run, which never blocks below the full tier.\n */\nexport async function runFullTierRules(\n input: SiteSystemRenderedInput,\n options: FullTierRuleOptions = {},\n): Promise<readonly SiteSystemFinding[]> {\n const settings = projectSettings(input.projectRoot);\n if (settings.tier !== null && settings.tier !== \"full\") return [];\n let geography: FullGeography | null = null;\n let geographyProblem = \"\";\n if (options.geography !== undefined) {\n geography = options.geography;\n if (!geography) geographyProblem = \"no geography was supplied\";\n } else {\n try {\n geography = await loadFullGeography();\n } catch (error) {\n geographyProblem = `site-core's geography could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n }\n const estate = readEstate({\n pages: input.pages,\n buildDir: options.buildDir ?? input.buildDir ?? join(input.projectRoot, \".next\"),\n geography,\n ...(settings.service ? { seeds: { [settings.service]: \"service\" } } : {}),\n });\n const today = sydneyDay(options.asOf ?? new Date());\n const context: FullContext = {\n estate,\n geography,\n geographyProblem,\n strategy: readFullStrategy(input.projectRoot, today),\n projectRoot: input.projectRoot,\n refreshExceptions: settings.refreshExceptions,\n today,\n defaultOrder: options.defaultOrder ?? (await loadDefaultOrder()),\n };\n const inbound = inboundLinks(estate);\n const findings = [\n ...checkK12(context),\n ...checkE5(context, inbound),\n ...checkE10(context),\n ...checkL3(context),\n ...checkL4(context),\n ...checkL6(context, inbound),\n ...checkL7(context),\n ...checkL8(context),\n ...checkL9(context),\n ...checkL10(context),\n ];\n // The copy is only for L3, L4 and L10; let it go before the source scan.\n for (const page of estate.pages) page.copy = undefined;\n return [...findings, ...checkS4(context), ...checkLocationIndexing(context), ...checkW3(context)];\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAmC,MAAM,iBAAiB,CAAC;AAG/F,OAAO,EAAoB,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAwC,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,OAAO,EAAE,oBAAoB,EAAoB,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAsE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,WAAW,EAAiE,MAAM,gBAAgB,CAAC;AACpJ,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC1H,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE/L,uFAAuF;AACvF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAiD,CAAC;AAavM,6GAA6G;AAC7G,MAAM,oBAAoB,GAAW,2CAA2C,CAAC;AAEjF,KAAK,UAAU,gBAAgB;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAEjD,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,CAAC,mBAAmB,CAAC;QAC5C,IAAI,OAAO,QAAQ,KAAK,UAAU;YAAE,OAAO,gFAAgF,CAAC;QAC5H,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;YACvB,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;gBACtC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC;YACvH,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,qDAAqD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACvH,CAAC;AACH,CAAC;AAED,mIAAmI;AACnI,SAAS,eAAe,CAAC,WAAmB;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;QACxD,mFAAmF;QACnF,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI;YAClE,OAAO,EAAE,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC;YAC/C,iBAAiB,EAAE,MAAM,CAAC,UAAU,EAAE,iBAAiB,IAAI,EAAE;SAC9D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAC;IACnE,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,SAAS,WAAW,CAAC,IAAoD;IACvE,MAAM,IAAI,GAAG,CAAC,OAAqD,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3J,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AACpF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA8B,EAC9B,UAA+B,EAAE;IAEjC,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACpD,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IAClE,IAAI,SAAS,GAAyB,IAAI,CAAC;IAC3C,IAAI,gBAAgB,GAAG,EAAE,CAAC;IAC1B,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACpC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QAC9B,IAAI,CAAC,SAAS;YAAE,gBAAgB,GAAG,2BAA2B,CAAC;IACjE,CAAC;SAAM,CAAC;QACN,IAAI,CAAC;YACH,SAAS,GAAG,MAAM,iBAAiB,EAAE,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,gBAAgB,GAAG,gDAAgD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9H,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,UAAU,CAAC;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,CAAC;QAChF,SAAS;QACT,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAC,CAAC;IACH,MAAM,OAAO,GAAgB;QAC3B,MAAM;QACN,SAAS;QACT,gBAAgB;QAChB,QAAQ;QACR,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,cAAc,EAAE,uBAAuB,CAAC,KAAK,CAAC,WAAW,CAAC;QAC1D,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;QAC7C,KAAK;QACL,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC,MAAM,gBAAgB,EAAE,CAAC;KACjE,CAAC;IACF,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG;QACf,GAAG,QAAQ,CAAC,OAAO,CAAC;QACpB,GAAG,aAAa,CAAC,OAAO,CAAC;QACzB,GAAG,cAAc,CAAC,OAAO,CAAC;QAC1B,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC;QAC5B,GAAG,QAAQ,CAAC,OAAO,CAAC;QACpB,GAAG,OAAO,CAAC,OAAO,CAAC;QACnB,GAAG,OAAO,CAAC,OAAO,CAAC;QACnB,GAAG,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC;QAC5B,GAAG,OAAO,CAAC,OAAO,CAAC;QACnB,GAAG,OAAO,CAAC,OAAO,CAAC;QACnB,GAAG,OAAO,CAAC,OAAO,CAAC;QACnB,GAAG,QAAQ,CAAC,OAAO,CAAC;KACrB,CAAC;IACF,yEAAyE;IACzE,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;IACvD,OAAO,CAAC,GAAG,QAAQ,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,qBAAqB,CAAC,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AACpG,CAAC","sourcesContent":["/**\n * The full-tier rules (release 3.12.0): page families, location estates, section manifests, links and the local data\n * contract. They read the production build's route catalogue and pages (`.next`), the project's strategy record and\n * source, and the site's installed site-core (geography and section orders), and return findings for the ready report:\n * K12 (families), K12-slots and E1-pattern (3.14.0), E5, E10, L3, L4, L6, L7, L8, L9, L10 (location), S4, the location indexing guard and, from 3.13.0,\n * W3 (estate refresh). The runner has the signature the ready gate registers.\n */\nimport { join } from \"node:path\";\nimport { readSiteSystemProjectConfig, type SiteSystemRefreshException } from \"../../config.js\";\nimport type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { type FullContext } from \"./common.js\";\nimport { checkE1Pattern } from \"./e1-pattern.js\";\nimport { checkE10 } from \"./e10-phrases.js\";\nimport { readEstate } from \"./estate.js\";\nimport { loadFullGeography, type FullGeography, type PathAliases } from \"./geography.js\";\nimport { checkK12 } from \"./k12-manifests.js\";\nimport { checkK12Slots } from \"./k12-slots.js\";\nimport { checkL3 } from \"./l3-neighbours.js\";\nimport { checkL4 } from \"./l4-local-copy.js\";\nimport { checkL9 } from \"./l9-data-contract.js\";\nimport { checkL10 } from \"./l10-location-copy.js\";\nimport { checkE5, checkL6, checkL7, checkL8, inboundLinks } from \"./links.js\";\nimport { readRouteCatalogueKinds } from \"./route-kinds.js\";\nimport { checkLocationIndexing, sydneyDay } from \"./loc-index.js\";\nimport { checkS4 } from \"./s4-route-files.js\";\nimport { readFullStrategy } from \"./strategy.js\";\nimport { checkW3 } from \"./w3-refresh.js\";\n\nexport { FULL_LISTED_FAILURES, type FullContext } from \"./common.js\";\nexport { headingPhrase } from \"./e10-phrases.js\";\nexport { readEstate, type Estate, type EstatePage, type LocationCopy, type ManifestData } from \"./estate.js\";\nexport { defineFullGeography, loadFullGeography, placeOfPath, type FullGeography, type FullGeographyInput, type PathAliases } from \"./geography.js\";\nexport { K12_FAMILIES, isDataBackedReason } from \"./k12-manifests.js\";\nexport { L3_MAX_LIST, L3_MIN_LIST, L3_TOLERANCE_KM } from \"./l3-neighbours.js\";\nexport { L4_MIN_ROTATION_PAGES, L4_MIN_TEMPLATE_WORDS } from \"./l4-local-copy.js\";\nexport { yearStamp } from \"./l10-location-copy.js\";\nexport { L6_MIN_INBOUND, L6_MIN_PAGE_TYPES, L7_MAX_DEPTH, clickDepths } from \"./links.js\";\nexport { CATALOGUE_HUB_FAMILIES, ROUTE_CATALOGUE_FILE, linkingPageType, readRouteCatalogueKinds } from \"./route-kinds.js\";\nexport { sydneyDay } from \"./loc-index.js\";\nexport { W3_FAMILIES, W3_MIN_REVALIDATE_SECONDS, refreshInterval } from \"./w3-refresh.js\";\nexport { ESTATE_PLAN_SNAPSHOT_FILE, FULL_STRATEGY_FILE, WAVE_MAX_GAP_DAYS, WAVE_MIN_GAP_DAYS, matchesWaveRoute, normalisePhrase, readFullStrategy, waveScheduleProblems } from \"./strategy.js\";\n\n/** The full-tier checks this module runs, in the order their findings are returned. */\nexport const FULL_TIER_CHECK_IDS = [\"K12\", \"K12-slots\", \"E1-pattern\", \"E5\", \"E10\", \"L3\", \"L4\", \"L6\", \"L7\", \"L8\", \"L9\", \"L10\", \"S4\", \"LOC-INDEX\", \"W3\"] as const satisfies readonly SiteSystemCheckId[];\n\nexport interface FullTierRuleOptions {\n /** The day waves are judged on. Defaults to now. */\n readonly asOf?: Date;\n /** The build directory; defaults to `input.buildDir`, then `<projectRoot>/.next`. */\n readonly buildDir?: string;\n /** Places for a test; a real run loads the site's site-core geography. Null runs without places. */\n readonly geography?: FullGeography | null;\n /** Canonical section ids per family for a test; a real run loads site-core's `defaultSectionOrder`. */\n readonly defaultOrder?: FullContext[\"defaultOrder\"];\n}\n\n// Variable specifiers, so the package compiles without site-core; they resolve to the site's installed copy.\nconst SECTION_ORDER_MODULE: string = \"@brandlab-digital/site-core/section-order\";\n\nasync function loadDefaultOrder(): Promise<FullContext[\"defaultOrder\"]> {\n try {\n const module = (await import(SECTION_ORDER_MODULE)) as {\n defaultSectionOrder?: (family: string, trade?: string) => { cap: number; entries: readonly { id: string; requirement: string }[] };\n };\n const defaults = module.defaultSectionOrder;\n if (typeof defaults !== \"function\") return \"the installed site-core has no defaultSectionOrder (3.11.0 or later is needed)\";\n return (family, trade) => {\n try {\n const order = defaults(family, trade);\n return { cap: order.cap, entries: order.entries.map((entry) => ({ id: entry.id, requirement: entry.requirement })) };\n } catch {\n return undefined;\n }\n };\n } catch (error) {\n return `site-core's section orders could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n}\n\n/** The declared tier, the representative service route and the W3 exceptions; null tier when the config cannot be read in full. */\nfunction projectSettings(projectRoot: string): { tier: string | null; service: string | undefined; refreshExceptions: readonly SiteSystemRefreshException[] } {\n try {\n const config = readSiteSystemProjectConfig(projectRoot);\n // A config the gate cannot read in full could hide a full tier, so the checks run.\n return {\n tier: config.complete ? (config.siteSystem?.tier ?? \"none\") : null,\n service: config.representativeRoutes[\"service\"],\n refreshExceptions: config.siteSystem?.refreshExceptions ?? [],\n };\n } catch {\n return { tier: null, service: undefined, refreshExceptions: [] };\n }\n}\n\n/** The estate plan's public slugs as path aliases: public slug → registry slug. */\nfunction pathAliases(plan: NonNullable<FullContext[\"strategy\"][\"estate\"]>): PathAliases {\n const flip = (renames: Readonly<Record<string, string>> | undefined) => new Map(Object.entries(renames ?? {}).map(([registry, slug]) => [slug, registry]));\n return { regions: flip(plan.slugs?.regions), suburbs: flip(plan.slugs?.suburbs) };\n}\n\n/**\n * Runs the full-tier rules over the built estate at `projectRoot`. A site that declares a lower tier (or none) gets no\n * findings: the report shows the full checks as not run, which never blocks below the full tier.\n */\nexport async function runFullTierRules(\n input: SiteSystemRenderedInput,\n options: FullTierRuleOptions = {},\n): Promise<readonly SiteSystemFinding[]> {\n const settings = projectSettings(input.projectRoot);\n if (settings.tier !== null && settings.tier !== \"full\") return [];\n let geography: FullGeography | null = null;\n let geographyProblem = \"\";\n if (options.geography !== undefined) {\n geography = options.geography;\n if (!geography) geographyProblem = \"no geography was supplied\";\n } else {\n try {\n geography = await loadFullGeography();\n } catch (error) {\n geographyProblem = `site-core's geography could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n }\n const today = sydneyDay(options.asOf ?? new Date());\n const strategy = readFullStrategy(input.projectRoot, today);\n const estate = readEstate({\n pages: input.pages,\n buildDir: options.buildDir ?? input.buildDir ?? join(input.projectRoot, \".next\"),\n geography,\n ...(settings.service ? { seeds: { [settings.service]: \"service\" } } : {}),\n ...(strategy.estate?.slugs ? { aliases: pathAliases(strategy.estate) } : {}),\n ...(strategy.estate?.paths ? { paths: strategy.estate.paths } : {}),\n });\n const context: FullContext = {\n estate,\n geography,\n geographyProblem,\n strategy,\n projectRoot: input.projectRoot,\n catalogueKinds: readRouteCatalogueKinds(input.projectRoot),\n refreshExceptions: settings.refreshExceptions,\n today,\n defaultOrder: options.defaultOrder ?? (await loadDefaultOrder()),\n };\n const inbound = inboundLinks(estate);\n const findings = [\n ...checkK12(context),\n ...checkK12Slots(context),\n ...checkE1Pattern(context),\n ...checkE5(context, inbound),\n ...checkE10(context),\n ...checkL3(context),\n ...checkL4(context),\n ...checkL6(context, inbound),\n ...checkL7(context),\n ...checkL8(context),\n ...checkL9(context),\n ...checkL10(context),\n ];\n // The copy is only for L3, L4 and L10; let it go before the source scan.\n for (const page of estate.pages) page.copy = undefined;\n return [...findings, ...checkS4(context), ...checkLocationIndexing(context), ...checkW3(context)];\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ import { type FullContext } from "./common.js";
3
+ export declare function checkK12Slots(context: FullContext): SiteSystemFinding[];
4
+ //# sourceMappingURL=k12-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k12-slots.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/k12-slots.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAG9F,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA8BvE"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * K12-slots (3.14.0): the lines a trade requires inside a section (site-core `SECTION_TRADE_PROFILES`, matrix part
3
+ * B), such as the tree trade's council permit note in `LocalFacts` on suburb and service-in-suburb pages. The trade is
4
+ * the one `defineSectionOrder` names in strategy.ts. Wherever the section renders, it carries the slot
5
+ * (`data-bl-slot="permit"`), or lists it in `data-bl-slot-skip` with a reason drawn from its data
6
+ * (`permit:no-current-council-record`), which passes with a warning so the record gets re-checked. A section that
7
+ * does not render is K12's business.
8
+ */
9
+ import { requiredSectionSlots } from "@brandlab-digital/site-core/section-order";
10
+ import { fail, notApplicable, outcome, plural, warning } from "./common.js";
11
+ import { isDataBackedReason } from "./k12-manifests.js";
12
+ export function checkK12Slots(context) {
13
+ const trade = context.strategy.trade;
14
+ const failures = [];
15
+ let checked = 0;
16
+ for (const page of context.estate.pages) {
17
+ if (!page.family)
18
+ continue;
19
+ const required = requiredSectionSlots(page.family, trade);
20
+ for (const { section, slot } of required) {
21
+ if (!page.sections.includes(section))
22
+ continue;
23
+ checked += 1;
24
+ const found = page.slots?.get(section);
25
+ if (found?.present.includes(slot))
26
+ continue;
27
+ const skip = found?.skipped.find((entry) => entry.slot === slot);
28
+ if (skip && isDataBackedReason(skip.reason)) {
29
+ failures.push(warning("K12-slots", page.path, `${page.path} leaves the ${slot} line out of its "${section}" section (${skip.reason}); bring the record up to date so it shows again.`));
30
+ continue;
31
+ }
32
+ failures.push(fail("K12-slots", page.path, `${page.path} renders "${section}" without the ${slot} line the ${trade} trade requires on ${page.family} pages, and gives no data-backed reason (${skip ? skip.reason : "none"}).`));
33
+ }
34
+ }
35
+ if (checked === 0) {
36
+ return [notApplicable("K12-slots", trade ? `The ${trade} trade requires no line inside the sections these pages render.` : "strategy.ts names no trade in defineSectionOrder, so no trade lines are required.")];
37
+ }
38
+ return outcome("K12-slots", failures, `${plural(checked, "required trade line")} ${checked === 1 ? "is" : "are"} in place.`);
39
+ }
40
+ //# sourceMappingURL=k12-slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k12-slots.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/k12-slots.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,oBAAoB,EAA+C,MAAM,2CAA2C,CAAC;AAE9H,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,UAAU,aAAa,CAAC,OAAoB;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAkC,CAAC;IAClE,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,SAAS;QAC3B,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,CAAC,MAA4B,EAAE,KAAK,CAAC,CAAC;QAChF,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC/C,OAAO,IAAI,CAAC,CAAC;YACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACvC,IAAI,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC5C,MAAM,IAAI,GAAG,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;YACjE,IAAI,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,eAAe,IAAI,qBAAqB,OAAO,cAAc,IAAI,CAAC,MAAM,mDAAmD,CAAC,CAAC,CAAC;gBACxL,SAAS;YACX,CAAC;YACD,QAAQ,CAAC,IAAI,CACX,IAAI,CACF,WAAW,EACX,IAAI,CAAC,IAAI,EACT,GAAG,IAAI,CAAC,IAAI,aAAa,OAAO,iBAAiB,IAAI,aAAa,KAAK,sBAAsB,IAAI,CAAC,MAAM,4CAA4C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,CACpL,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;QAClB,OAAO,CAAC,aAAa,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,KAAK,iEAAiE,CAAC,CAAC,CAAC,mFAAmF,CAAC,CAAC,CAAC;IACnN,CAAC;IACD,OAAO,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,qBAAqB,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC;AAC/H,CAAC","sourcesContent":["/**\n * K12-slots (3.14.0): the lines a trade requires inside a section (site-core `SECTION_TRADE_PROFILES`, matrix part\n * B), such as the tree trade's council permit note in `LocalFacts` on suburb and service-in-suburb pages. The trade is\n * the one `defineSectionOrder` names in strategy.ts. Wherever the section renders, it carries the slot\n * (`data-bl-slot=\"permit\"`), or lists it in `data-bl-slot-skip` with a reason drawn from its data\n * (`permit:no-current-council-record`), which passes with a warning so the record gets re-checked. A section that\n * does not render is K12's business.\n */\nimport { requiredSectionSlots, type BrandlabPageFamily, type BrandlabTrade } from \"@brandlab-digital/site-core/section-order\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, outcome, plural, warning, type FullContext } from \"./common.js\";\nimport { isDataBackedReason } from \"./k12-manifests.js\";\n\nexport function checkK12Slots(context: FullContext): SiteSystemFinding[] {\n const trade = context.strategy.trade as BrandlabTrade | undefined;\n const failures: SiteSystemFinding[] = [];\n let checked = 0;\n for (const page of context.estate.pages) {\n if (!page.family) continue;\n const required = requiredSectionSlots(page.family as BrandlabPageFamily, trade);\n for (const { section, slot } of required) {\n if (!page.sections.includes(section)) continue;\n checked += 1;\n const found = page.slots?.get(section);\n if (found?.present.includes(slot)) continue;\n const skip = found?.skipped.find((entry) => entry.slot === slot);\n if (skip && isDataBackedReason(skip.reason)) {\n failures.push(warning(\"K12-slots\", page.path, `${page.path} leaves the ${slot} line out of its \"${section}\" section (${skip.reason}); bring the record up to date so it shows again.`));\n continue;\n }\n failures.push(\n fail(\n \"K12-slots\",\n page.path,\n `${page.path} renders \"${section}\" without the ${slot} line the ${trade} trade requires on ${page.family} pages, and gives no data-backed reason (${skip ? skip.reason : \"none\"}).`,\n ),\n );\n }\n }\n if (checked === 0) {\n return [notApplicable(\"K12-slots\", trade ? `The ${trade} trade requires no line inside the sections these pages render.` : \"strategy.ts names no trade in defineSectionOrder, so no trade lines are required.\")];\n }\n return outcome(\"K12-slots\", failures, `${plural(checked, \"required trade line\")} ${checked === 1 ? \"is\" : \"are\"} in place.`);\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  * L10 for location copy (Site System v2 §4.4 step 4, banned on location pages): no kilometre figures, no "km from"
3
3
  * and no "based in" anywhere in a suburb, service-in-suburb or region page's content (verbatim reviews aside), and no
4
4
  * year stamp posing as local detail: a year in a line that names a place, outside a "Month Year" job or review date,
5
- * a price table and the proof strip. The patterns are the shell's (L10-hero), so the two checks never disagree.
5
+ * a price table and the proof strip, and outside a financial year on a council fee ("$201 (2026/27)"). The patterns are the shell's (L10-hero), so the two checks never disagree.
6
6
  */
7
7
  import type { SiteSystemFinding } from "../../checks.js";
8
8
  import { type FullContext } from "./common.js";
@@ -1 +1 @@
1
- {"version":3,"file":"l10-location-copy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l10-location-copy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AASrF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAqB,GAAG,MAAM,GAAG,IAAI,CASxG;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAwBlE"}
1
+ {"version":3,"file":"l10-location-copy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l10-location-copy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAYrF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAqB,GAAG,MAAM,GAAG,IAAI,CASxG;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAwBlE"}
@@ -2,8 +2,11 @@ import { BASED_IN, KM_FIGURE, KM_FROM } from "../shell/location-wording.js";
2
2
  import { fail, notApplicable, outcome, plural } from "./common.js";
3
3
  import { isLocationFamily } from "./estate.js";
4
4
  const MONTH = "(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)";
5
- /** A year from 1800 to 2099, or a decade ("1950s"), not part of a price, a longer number or a postcode. */
6
- const YEAR = /(?<![$\d.,/:-])\b(1[89]\d\d|20\d\d)(?:'?s)?\b(?![.,]?\d)/gi;
5
+ /**
6
+ * A year from 1800 to 2099, or a decade ("1950s"), not part of a price, a longer number or a postcode, and not a
7
+ * financial year ("2026/27" or "2026–27"): council fees carry the year they are for (3.15.0, the permit note).
8
+ */
9
+ const YEAR = /(?<![$\d.,/:-])\b(1[89]\d\d|20\d\d)(?:'?s)?\b(?![.,]?\d|[/\u2013]\d{2}(?![\d/]))/gi;
7
10
  const BEFORE_YEAR_ALLOWED = new RegExp(`(?:${MONTH}\\.?|nsw|postcode|©|\\(c\\))\\s*$`, "i");
8
11
  const NO_YEAR_SECTIONS = new Set(["reviews", "nearby-jobs", "price-guide", "proof-strip"]);
9
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"l10-location-copy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l10-location-copy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L10 for location copy (Site System v2 §4.4 step 4, banned on location pages): no kilometre figures, no \"km from\"\n * and no \"based in\" anywhere in a suburb, service-in-suburb or region page's content (verbatim reviews aside), and no\n * year stamp posing as local detail: a year in a line that names a place, outside a \"Month Year\" job or review date,\n * a price table and the proof strip. The patterns are the shell's (L10-hero), so the two checks never disagree.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { BASED_IN, KM_FIGURE, KM_FROM } from \"../shell/location-wording.js\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily } from \"./estate.js\";\n\nconst MONTH = \"(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\";\n/** A year from 1800 to 2099, or a decade (\"1950s\"), not part of a price, a longer number or a postcode. */\nconst YEAR = /(?<![$\\d.,/:-])\\b(1[89]\\d\\d|20\\d\\d)(?:'?s)?\\b(?![.,]?\\d)/gi;\nconst BEFORE_YEAR_ALLOWED = new RegExp(`(?:${MONTH}\\\\.?|nsw|postcode|©|\\\\(c\\\\))\\\\s*$`, \"i\");\nconst NO_YEAR_SECTIONS = new Set([\"reviews\", \"nearby-jobs\", \"price-guide\", \"proof-strip\"]);\n\n/**\n * The first year stamp in a line, or null. A number after a month is a date, and a number straight after a place\n * name (\"Abbotsford 2046\", \"Ryde NSW 2112\") is a postcode; `isPlace` says whether some words are a place name.\n */\nexport function yearStamp(line: string, isPlace: (words: string) => boolean = () => false): string | null {\n for (const match of line.matchAll(YEAR)) {\n const before = line.slice(0, match.index);\n if (BEFORE_YEAR_ALLOWED.test(before)) continue;\n const words = before.replace(/[,\\s]+$/, \"\").split(/\\s+/);\n if ([1, 2, 3, 4].some((count) => words.length >= count && isPlace(words.slice(-count).join(\" \")))) continue;\n return match[0];\n }\n return null;\n}\n\nexport function checkL10(context: FullContext): SiteSystemFinding[] {\n const geography = context.geography;\n const failures: SiteSystemFinding[] = [];\n let checked = 0;\n for (const page of context.estate.pages) {\n if (!isLocationFamily(page.family) || !page.copy) continue;\n checked += 1;\n // One example of each kind of problem.\n const found = new Map<string, string>();\n const isPlace = (words: string) => geography?.names.has(words.toLowerCase()) === true;\n for (const line of page.copy.lines) {\n if (line.section === \"reviews\") continue;\n const km = KM_FIGURE.exec(line.text) ?? KM_FROM.exec(line.text);\n if (km && !found.has(\"km\")) found.set(\"km\", `a kilometre figure (\"${km[0]}\")`);\n if (BASED_IN.test(line.text)) found.set(\"based\", `\"based in\"`);\n if (NO_YEAR_SECTIONS.has(line.section) || !geography || found.has(\"year\")) continue;\n const year = yearStamp(line.text, isPlace);\n if (year && geography.placesIn(line.text).length > 0) found.set(\"year\", `a year stamp as local detail (\"${year}\" in \"${line.text.slice(0, 80)}\")`);\n }\n if (found.size > 0) failures.push(fail(\"L10\", page.path, `The location copy on ${page.path} has ${[...found.values()].join(\" and \")}, which location pages do not carry.`));\n }\n if (checked === 0) return [notApplicable(\"L10\", \"No location page was read, so there is no location copy to check.\")];\n if (!geography) failures.push(fail(\"L10\", undefined, `The place names could not be read (${context.geographyProblem}), so year stamps posing as local detail could not be checked.`));\n return outcome(\"L10\", failures, `No kilometre figures, \"km from\", \"based in\" or year stamps as local detail in the copy of ${plural(checked, \"location page\")}.`);\n}\n"]}
1
+ {"version":3,"file":"l10-location-copy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l10-location-copy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L10 for location copy (Site System v2 §4.4 step 4, banned on location pages): no kilometre figures, no \"km from\"\n * and no \"based in\" anywhere in a suburb, service-in-suburb or region page's content (verbatim reviews aside), and no\n * year stamp posing as local detail: a year in a line that names a place, outside a \"Month Year\" job or review date,\n * a price table and the proof strip, and outside a financial year on a council fee (\"$201 (2026/27)\"). The patterns are the shell's (L10-hero), so the two checks never disagree.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { BASED_IN, KM_FIGURE, KM_FROM } from \"../shell/location-wording.js\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily } from \"./estate.js\";\n\nconst MONTH = \"(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\";\n/**\n * A year from 1800 to 2099, or a decade (\"1950s\"), not part of a price, a longer number or a postcode, and not a\n * financial year (\"2026/27\" or \"2026–27\"): council fees carry the year they are for (3.15.0, the permit note).\n */\nconst YEAR = /(?<![$\\d.,/:-])\\b(1[89]\\d\\d|20\\d\\d)(?:'?s)?\\b(?![.,]?\\d|[/\\u2013]\\d{2}(?![\\d/]))/gi;\nconst BEFORE_YEAR_ALLOWED = new RegExp(`(?:${MONTH}\\\\.?|nsw|postcode|©|\\\\(c\\\\))\\\\s*$`, \"i\");\nconst NO_YEAR_SECTIONS = new Set([\"reviews\", \"nearby-jobs\", \"price-guide\", \"proof-strip\"]);\n\n/**\n * The first year stamp in a line, or null. A number after a month is a date, and a number straight after a place\n * name (\"Abbotsford 2046\", \"Ryde NSW 2112\") is a postcode; `isPlace` says whether some words are a place name.\n */\nexport function yearStamp(line: string, isPlace: (words: string) => boolean = () => false): string | null {\n for (const match of line.matchAll(YEAR)) {\n const before = line.slice(0, match.index);\n if (BEFORE_YEAR_ALLOWED.test(before)) continue;\n const words = before.replace(/[,\\s]+$/, \"\").split(/\\s+/);\n if ([1, 2, 3, 4].some((count) => words.length >= count && isPlace(words.slice(-count).join(\" \")))) continue;\n return match[0];\n }\n return null;\n}\n\nexport function checkL10(context: FullContext): SiteSystemFinding[] {\n const geography = context.geography;\n const failures: SiteSystemFinding[] = [];\n let checked = 0;\n for (const page of context.estate.pages) {\n if (!isLocationFamily(page.family) || !page.copy) continue;\n checked += 1;\n // One example of each kind of problem.\n const found = new Map<string, string>();\n const isPlace = (words: string) => geography?.names.has(words.toLowerCase()) === true;\n for (const line of page.copy.lines) {\n if (line.section === \"reviews\") continue;\n const km = KM_FIGURE.exec(line.text) ?? KM_FROM.exec(line.text);\n if (km && !found.has(\"km\")) found.set(\"km\", `a kilometre figure (\"${km[0]}\")`);\n if (BASED_IN.test(line.text)) found.set(\"based\", `\"based in\"`);\n if (NO_YEAR_SECTIONS.has(line.section) || !geography || found.has(\"year\")) continue;\n const year = yearStamp(line.text, isPlace);\n if (year && geography.placesIn(line.text).length > 0) found.set(\"year\", `a year stamp as local detail (\"${year}\" in \"${line.text.slice(0, 80)}\")`);\n }\n if (found.size > 0) failures.push(fail(\"L10\", page.path, `The location copy on ${page.path} has ${[...found.values()].join(\" and \")}, which location pages do not carry.`));\n }\n if (checked === 0) return [notApplicable(\"L10\", \"No location page was read, so there is no location copy to check.\")];\n if (!geography) failures.push(fail(\"L10\", undefined, `The place names could not be read (${context.geographyProblem}), so year stamps posing as local detail could not be checked.`));\n return outcome(\"L10\", failures, `No kilometre figures, \"km from\", \"based in\" or year stamps as local detail in the copy of ${plural(checked, \"location page\")}.`);\n}\n"]}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * L3, neighbours by real distance (Site System v2 §4.4 D8). A location page's neighbour list (a list of 3 to 12 links
3
3
  * to other pages of its own family) names the nearest suburbs that have a page of that family, nearest first, by the
4
- * distance between suburb centres: never alphabetical rings or seeded shuffles. Only indexable pages count as
5
- * candidates, so a page held for a later wave is never expected in a list. A line of copy that names three or more
4
+ * distance between suburb centres: never alphabetical rings or seeded shuffles. Only pages the site may link count as
5
+ * candidates: indexable, and, with an estate plan, linked now by `estateLinkState` (indexed now or its wave due), so a
6
+ * page held for a later wave, which Coverage and the neighbour lists name as plain text, is never expected in a list. A line of copy that names three or more
6
7
  * nearby suburbs ("Nearby areas include …") is held to the same order, so an alphabetical ring in text fails too.
7
8
  */
8
9
  import type { SiteSystemFinding } from "../../checks.js";
@@ -1 +1 @@
1
- {"version":3,"file":"l3-neighbours.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l3-neighbours.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAIrF,oEAAoE;AACpE,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yDAAyD;AACzD,eAAO,MAAM,eAAe,MAAM,CAAC;AAGnC,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA2DjE"}
1
+ {"version":3,"file":"l3-neighbours.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l3-neighbours.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAIrF,oEAAoE;AACpE,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yDAAyD;AACzD,eAAO,MAAM,eAAe,MAAM,CAAC;AAGnC,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA6DjE"}
@@ -1,4 +1,5 @@
1
1
  import { distanceBetweenCentresKm, findNeighbourOrderProblem } from "../../../page-standard.js";
2
+ import { estateLinkState } from "@brandlab-digital/site-core/strategy";
2
3
  import { fail, notApplicable, outcome, plural } from "./common.js";
3
4
  import { isLocationFamily } from "./estate.js";
4
5
  import { placeName } from "./geography.js";
@@ -18,6 +19,8 @@ export function checkL3(context) {
18
19
  continue;
19
20
  byStem.set(page.place.stem, [...(byStem.get(page.place.stem) ?? []), page]);
20
21
  }
22
+ const plan = context.strategy.estate;
23
+ const linkable = (path) => !plan || estateLinkState(path, { estate: plan, waves: context.strategy.waves, on: context.today }) === "link";
21
24
  const failures = [];
22
25
  let lists = 0;
23
26
  for (const page of estate.pages) {
@@ -62,7 +65,7 @@ export function checkL3(context) {
62
65
  };
63
66
  const farthest = Math.max(...listed.map(distance).filter(Number.isFinite));
64
67
  const nearer = family
65
- .filter((other) => other !== page && other.indexable && other.place && !listed.includes(other.place.slug) && other.place.slug !== place.slug)
68
+ .filter((other) => other !== page && other.indexable && linkable(other.path) && other.place && !listed.includes(other.place.slug) && other.place.slug !== place.slug)
66
69
  .map((other) => ({ slug: other.place.slug, km: distance(other.place.slug) }))
67
70
  .filter((other) => other.km + L3_TOLERANCE_KM < farthest)
68
71
  .sort((left, right) => left.km - right.km)[0];
@@ -1 +1 @@
1
- {"version":3,"file":"l3-neighbours.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l3-neighbours.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L3, neighbours by real distance (Site System v2 §4.4 D8). A location page's neighbour list (a list of 3 to 12 links\n * to other pages of its own family) names the nearest suburbs that have a page of that family, nearest first, by the\n * distance between suburb centres: never alphabetical rings or seeded shuffles. Only indexable pages count as\n * candidates, so a page held for a later wave is never expected in a list. A line of copy that names three or more\n * nearby suburbs (\"Nearby areas include …\") is held to the same order, so an alphabetical ring in text fails too.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { distanceBetweenCentresKm, findNeighbourOrderProblem } from \"../../../page-standard.js\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type EstatePage } from \"./estate.js\";\nimport { placeName } from \"./geography.js\";\n\n/** A list longer than this is a directory, not a neighbour list. */\nexport const L3_MAX_LIST = 12;\nexport const L3_MIN_LIST = 3;\n/** A nearer page left out by less than this is a tie. */\nexport const L3_TOLERANCE_KM = 0.5;\nconst NEARBY_WORDS = /\\b(?:nearby|neighbouring|neighboring|surrounding|nearest|close by)\\b/i;\n\nexport function checkL3(context: FullContext): SiteSystemFinding[] {\n const { estate, geography } = context;\n if (!geography) return [fail(\"L3\", undefined, `The suburb centres could not be read (${context.geographyProblem}), so neighbour lists could not be checked.`)];\n const byStem = new Map<string, EstatePage[]>();\n for (const page of estate.pages) {\n if (!isLocationFamily(page.family) || page.place?.kind !== \"suburb\") continue;\n byStem.set(page.place.stem, [...(byStem.get(page.place.stem) ?? []), page]);\n }\n const failures: SiteSystemFinding[] = [];\n let lists = 0;\n for (const page of estate.pages) {\n const place = page.place;\n if (!isLocationFamily(page.family) || place?.kind !== \"suburb\") continue;\n const origin = geography.centreOf(place.slug);\n if (!origin) continue;\n const family = byStem.get(place.stem) ?? [];\n for (const line of page.copy?.lines ?? []) {\n if (line.section === \"reviews\" || line.section === \"nearby-jobs\" || !NEARBY_WORDS.test(line.text)) continue;\n const named = geography.placesIn(line.text).filter((slug) => slug !== place.slug && geography.suburbs.has(slug) && geography.centreOf(slug));\n if (named.length < L3_MIN_LIST || named.length > L3_MAX_LIST) continue;\n lists += 1;\n const order = findNeighbourOrderProblem(place.slug, named, geography.centreOf);\n if (order) failures.push(fail(\"L3\", page.path, `The nearby suburbs named on ${page.path} are not nearest first: ${order.detail}.`));\n }\n for (const group of page.linkGroups) {\n const listed: string[] = [];\n for (const target of group) {\n const other = estate.byPath.get(target);\n if (!other || other === page || other.place?.stem !== place.stem || other.place.kind !== \"suburb\") continue;\n if (!listed.includes(other.place.slug)) listed.push(other.place.slug);\n }\n if (listed.length < L3_MIN_LIST || listed.length > L3_MAX_LIST) continue;\n lists += 1;\n const order = findNeighbourOrderProblem(place.slug, listed, geography.centreOf);\n if (order) {\n failures.push(fail(\"L3\", page.path, `The neighbour list on ${page.path} is not nearest first: ${order.detail}.`));\n continue;\n }\n const distance = (slug: string) => {\n const centre = geography.centreOf(slug);\n return centre ? distanceBetweenCentresKm(origin, centre) : Number.POSITIVE_INFINITY;\n };\n const farthest = Math.max(...listed.map(distance).filter(Number.isFinite));\n const nearer = family\n .filter((other) => other !== page && other.indexable && other.place && !listed.includes(other.place.slug) && other.place.slug !== place.slug)\n .map((other) => ({ slug: other.place!.slug, km: distance(other.place!.slug) }))\n .filter((other) => other.km + L3_TOLERANCE_KM < farthest)\n .sort((left, right) => left.km - right.km)[0];\n if (nearer) {\n failures.push(fail(\n \"L3\",\n page.path,\n `The neighbour list on ${page.path} leaves out ${placeName(nearer.slug, geography)} (${nearer.km.toFixed(1)} km), which has a page and is nearer than suburbs it lists (up to ${farthest.toFixed(1)} km).`,\n ));\n }\n }\n }\n if (lists === 0) return [notApplicable(\"L3\", \"No location page lists 3 to 12 neighbours of its own family.\")];\n return outcome(\"L3\", failures, `${plural(lists, \"neighbour list\")} on location pages name the nearest suburbs with a page, nearest first.`);\n}\n"]}
1
+ {"version":3,"file":"l3-neighbours.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l3-neighbours.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAChG,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAoB,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAmB,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC9B,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,yDAAyD;AACzD,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AACnC,MAAM,YAAY,GAAG,uEAAuE,CAAC;AAE7F,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC1C,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IACtC,IAAI,CAAC,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,yCAAyC,OAAO,CAAC,gBAAgB,6CAA6C,CAAC,CAAC,CAAC;IAC/J,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,QAAQ;YAAE,SAAS;QAC9E,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACrC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IACjJ,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,EAAE,IAAI,KAAK,QAAQ;YAAE,SAAS;QACzE,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,aAAa,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC5G,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7I,IAAI,KAAK,CAAC,MAAM,GAAG,WAAW,IAAI,KAAK,CAAC,MAAM,GAAG,WAAW;gBAAE,SAAS;YACvE,KAAK,IAAI,CAAC,CAAC;YACX,MAAM,KAAK,GAAG,yBAAyB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;YAC/E,IAAI,KAAK;gBAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,+BAA+B,IAAI,CAAC,IAAI,2BAA2B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACtI,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpC,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;gBAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACxC,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ;oBAAE,SAAS;gBAC5G,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACxE,CAAC;YACD,IAAI,MAAM,CAAC,MAAM,GAAG,WAAW,IAAI,MAAM,CAAC,MAAM,GAAG,WAAW;gBAAE,SAAS;YACzE,KAAK,IAAI,CAAC,CAAC;YACX,MAAM,KAAK,GAAG,yBAAyB,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;YAChF,IAAI,KAAK,EAAE,CAAC;gBACV,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,yBAAyB,IAAI,CAAC,IAAI,0BAA0B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAClH,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE;gBAChC,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACxC,OAAO,MAAM,CAAC,CAAC,CAAC,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;YACtF,CAAC,CAAC;YACF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC3E,MAAM,MAAM,GAAG,MAAM;iBAClB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;iBACpK,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAM,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;iBAC9E,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,eAAe,GAAG,QAAQ,CAAC;iBACxD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,IAAI,MAAM,EAAE,CAAC;gBACX,QAAQ,CAAC,IAAI,CAAC,IAAI,CAChB,IAAI,EACJ,IAAI,CAAC,IAAI,EACT,yBAAyB,IAAI,CAAC,IAAI,eAAe,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,qEAAqE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAC3M,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,8DAA8D,CAAC,CAAC,CAAC;IAC9G,OAAO,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,gBAAgB,CAAC,yEAAyE,CAAC,CAAC;AAC9I,CAAC","sourcesContent":["/**\n * L3, neighbours by real distance (Site System v2 §4.4 D8). A location page's neighbour list (a list of 3 to 12 links\n * to other pages of its own family) names the nearest suburbs that have a page of that family, nearest first, by the\n * distance between suburb centres: never alphabetical rings or seeded shuffles. Only pages the site may link count as\n * candidates: indexable, and, with an estate plan, linked now by `estateLinkState` (indexed now or its wave due), so a\n * page held for a later wave, which Coverage and the neighbour lists name as plain text, is never expected in a list. A line of copy that names three or more\n * nearby suburbs (\"Nearby areas include …\") is held to the same order, so an alphabetical ring in text fails too.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { distanceBetweenCentresKm, findNeighbourOrderProblem } from \"../../../page-standard.js\";\nimport { estateLinkState } from \"@brandlab-digital/site-core/strategy\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type EstatePage } from \"./estate.js\";\nimport { placeName } from \"./geography.js\";\n\n/** A list longer than this is a directory, not a neighbour list. */\nexport const L3_MAX_LIST = 12;\nexport const L3_MIN_LIST = 3;\n/** A nearer page left out by less than this is a tie. */\nexport const L3_TOLERANCE_KM = 0.5;\nconst NEARBY_WORDS = /\\b(?:nearby|neighbouring|neighboring|surrounding|nearest|close by)\\b/i;\n\nexport function checkL3(context: FullContext): SiteSystemFinding[] {\n const { estate, geography } = context;\n if (!geography) return [fail(\"L3\", undefined, `The suburb centres could not be read (${context.geographyProblem}), so neighbour lists could not be checked.`)];\n const byStem = new Map<string, EstatePage[]>();\n for (const page of estate.pages) {\n if (!isLocationFamily(page.family) || page.place?.kind !== \"suburb\") continue;\n byStem.set(page.place.stem, [...(byStem.get(page.place.stem) ?? []), page]);\n }\n const plan = context.strategy.estate;\n const linkable = (path: string) => !plan || estateLinkState(path, { estate: plan, waves: context.strategy.waves, on: context.today }) === \"link\";\n const failures: SiteSystemFinding[] = [];\n let lists = 0;\n for (const page of estate.pages) {\n const place = page.place;\n if (!isLocationFamily(page.family) || place?.kind !== \"suburb\") continue;\n const origin = geography.centreOf(place.slug);\n if (!origin) continue;\n const family = byStem.get(place.stem) ?? [];\n for (const line of page.copy?.lines ?? []) {\n if (line.section === \"reviews\" || line.section === \"nearby-jobs\" || !NEARBY_WORDS.test(line.text)) continue;\n const named = geography.placesIn(line.text).filter((slug) => slug !== place.slug && geography.suburbs.has(slug) && geography.centreOf(slug));\n if (named.length < L3_MIN_LIST || named.length > L3_MAX_LIST) continue;\n lists += 1;\n const order = findNeighbourOrderProblem(place.slug, named, geography.centreOf);\n if (order) failures.push(fail(\"L3\", page.path, `The nearby suburbs named on ${page.path} are not nearest first: ${order.detail}.`));\n }\n for (const group of page.linkGroups) {\n const listed: string[] = [];\n for (const target of group) {\n const other = estate.byPath.get(target);\n if (!other || other === page || other.place?.stem !== place.stem || other.place.kind !== \"suburb\") continue;\n if (!listed.includes(other.place.slug)) listed.push(other.place.slug);\n }\n if (listed.length < L3_MIN_LIST || listed.length > L3_MAX_LIST) continue;\n lists += 1;\n const order = findNeighbourOrderProblem(place.slug, listed, geography.centreOf);\n if (order) {\n failures.push(fail(\"L3\", page.path, `The neighbour list on ${page.path} is not nearest first: ${order.detail}.`));\n continue;\n }\n const distance = (slug: string) => {\n const centre = geography.centreOf(slug);\n return centre ? distanceBetweenCentresKm(origin, centre) : Number.POSITIVE_INFINITY;\n };\n const farthest = Math.max(...listed.map(distance).filter(Number.isFinite));\n const nearer = family\n .filter((other) => other !== page && other.indexable && linkable(other.path) && other.place && !listed.includes(other.place.slug) && other.place.slug !== place.slug)\n .map((other) => ({ slug: other.place!.slug, km: distance(other.place!.slug) }))\n .filter((other) => other.km + L3_TOLERANCE_KM < farthest)\n .sort((left, right) => left.km - right.km)[0];\n if (nearer) {\n failures.push(fail(\n \"L3\",\n page.path,\n `The neighbour list on ${page.path} leaves out ${placeName(nearer.slug, geography)} (${nearer.km.toFixed(1)} km), which has a page and is nearer than suburbs it lists (up to ${farthest.toFixed(1)} km).`,\n ));\n }\n }\n }\n if (lists === 0) return [notApplicable(\"L3\", \"No location page lists 3 to 12 neighbours of its own family.\")];\n return outcome(\"L3\", failures, `${plural(lists, \"neighbour list\")} on location pages name the nearest suburbs with a page, nearest first.`);\n}\n"]}
@@ -9,7 +9,8 @@
9
9
  * - no name-swapped FAQs: the same question and answer, naming the page's own place, on pages in two or more councils
10
10
  * (or regions) with only the place names changed; an answer drawn from council data stays the same within its
11
11
  * council, which is data, not a name swap;
12
- * - no other suburb's job without the "nearby" label in the nearby-jobs section.
12
+ * - no other suburb's job without the "nearby" label in the nearby-jobs section. The job's suburb is the card's
13
+ * `data-suburb` (NearbyJobs); a card without it is read for place names, never a species such as "Sydney blue gum".
13
14
  * A local line is one that names the page's own place; lines of 12 words or more are compared with the page's own
14
15
  * place and every number masked (FAQs with every place name and postcode masked). Reviews and job cards are left out
15
16
  * of the template comparison.
@@ -1 +1 @@
1
- {"version":3,"file":"l4-local-copy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l4-local-copy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAIrF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,+GAA+G;AAC/G,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,iHAAiH;AACjH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAgIvC,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAiBjE"}
1
+ {"version":3,"file":"l4-local-copy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l4-local-copy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAIrF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,+GAA+G;AAC/G,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,iHAAiH;AACjH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAqIvC,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAiBjE"}
@@ -1,6 +1,6 @@
1
1
  import { fail, notApplicable, outcome, plural } from "./common.js";
2
2
  import { isLocationFamily } from "./estate.js";
3
- import { placeName } from "./geography.js";
3
+ import { placeName, withoutCompoundNames } from "./geography.js";
4
4
  export const L4_MIN_TEMPLATE_WORDS = 12;
5
5
  /** Two templates sharing this much of their wording at the start and end are one sentence with a data slot. */
6
6
  export const L4_SLOT_SHARE = 0.6;
@@ -118,12 +118,17 @@ function faqFindings(pages, geography) {
118
118
  .filter((entry) => entry.paths.length >= 2 && entry.areas.size >= 2)
119
119
  .map((entry) => fail("L4", entry.paths[0], `The FAQ "${entry.question}" on ${entry.paths[0]} is name-swapped: the same question and answer appear on ${plural(entry.paths.length - 1, "other page")} (such as ${entry.paths[1]}) with the place name changed.`));
120
120
  }
121
+ /**
122
+ * A job's place comes from the card's structured data (`data-suburb`, the job log's suburb). Only a card without it
123
+ * is read for place names, with place-named species ("Sydney blue gum") masked first.
124
+ */
121
125
  function jobFindings(page, geography) {
122
126
  const findings = [];
123
127
  for (const card of page.copy.jobs ?? []) {
124
- if (/\bnearby\b/i.test(card))
128
+ if (/\bnearby\b/i.test(card.text))
125
129
  continue;
126
- const other = geography.placesIn(card).find((slug) => slug !== page.place.slug && geography.suburbs.has(slug));
130
+ const places = card.place !== undefined ? [card.place] : geography.placesIn(withoutCompoundNames(card.text));
131
+ const other = places.find((slug) => slug !== page.place.slug && geography.suburbs.has(slug));
127
132
  if (other) {
128
133
  findings.push(fail("L4", page.path, `${page.path} shows a job in ${placeName(other, geography)} without the "nearby" label.`));
129
134
  }
@@ -1 +1 @@
1
- {"version":3,"file":"l4-local-copy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l4-local-copy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L4, local copy that is really local (Site System v2 §4.3 item 5, §4.4 step 4), compared across the sibling pages of\n * each family (pages sharing a stem such as `/services/decking/{place}`):\n * - no copy rotated by page address: one section carrying two or more templates of a local sentence (the page's own\n * suburb swapped in) that never appear together, each on two or more pages and a tenth of the family, that name\n * the same other places, alternate inside one region and are worded differently. Variants keyed to an area, or one\n * slot filled from data (\"about 5 km\" against \"just down the road\"), are data, not rotation (L10 catches distance\n * wording);\n * - no name-swapped FAQs: the same question and answer, naming the page's own place, on pages in two or more councils\n * (or regions) with only the place names changed; an answer drawn from council data stays the same within its\n * council, which is data, not a name swap;\n * - no other suburb's job without the \"nearby\" label in the nearby-jobs section.\n * A local line is one that names the page's own place; lines of 12 words or more are compared with the page's own\n * place and every number masked (FAQs with every place name and postcode masked). Reviews and job cards are left out\n * of the template comparison.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type EstatePage } from \"./estate.js\";\nimport { placeName, type FullGeography } from \"./geography.js\";\n\nexport const L4_MIN_TEMPLATE_WORDS = 12;\n/** Two templates sharing this much of their wording at the start and end are one sentence with a data slot. */\nexport const L4_SLOT_SHARE = 0.6;\n/** Each rotated template is on at least this share of its family's pages (hash rotation spreads them evenly). */\nexport const L4_ROTATION_SHARE = 0.1;\n/** A family needs this many pages before rotation can be told from coincidence. */\nexport const L4_MIN_ROTATION_PAGES = 4;\nconst NOT_TEMPLATED = new Set([\"faqs\", \"nearby-jobs\", \"reviews\"]);\n\nfunction namesPlace(text: string, slug: string, geography: FullGeography): boolean {\n return geography.placesIn(text).includes(slug);\n}\n\n/** A line with only the page's own place and the numbers masked, so lines keyed to another area stay apart. */\nfunction ownTemplate(text: string, page: EstatePage, geography: FullGeography): string {\n const name = placeName(page.place!.slug, geography).toLowerCase().replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\s+/g, \"\\\\s+\");\n return text\n .toLowerCase()\n .replace(new RegExp(`\\\\b(?:the\\\\s+)?${name}\\\\b`, \"g\"), \"{place}\")\n .replace(/\\d+(?:[.,]\\d+)*/g, \"#\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/** The share of the shorter template's words that both templates open and close with. */\nfunction sharedFrame(left: string, right: string): number {\n const a = left.split(\" \");\n const b = right.split(\" \");\n const shortest = Math.min(a.length, b.length);\n let start = 0;\n while (start < shortest && a[start] === b[start]) start += 1;\n let end = 0;\n while (end < shortest - start && a[a.length - 1 - end] === b[b.length - 1 - end]) end += 1;\n return shortest === 0 ? 1 : (start + end) / shortest;\n}\n\nfunction regionOf(page: EstatePage, geography: FullGeography): string {\n const place = page.place!;\n return place.kind === \"region\" ? place.slug : (geography.suburbs.get(place.slug)?.region ?? place.slug);\n}\n\nfunction siblings(pages: readonly EstatePage[]): Map<string, EstatePage[]> {\n const groups = new Map<string, EstatePage[]>();\n for (const page of pages) {\n if (!isLocationFamily(page.family) || !page.place || !page.copy) continue;\n // Suburb pages compare with suburb pages, hubs with hubs.\n const key = `${page.place.stem}|${page.place.kind}`;\n groups.set(key, [...(groups.get(key) ?? []), page]);\n }\n return groups;\n}\n\nfunction rotationFindings(stem: string, pages: readonly EstatePage[], geography: FullGeography): SiteSystemFinding[] {\n if (pages.length < L4_MIN_ROTATION_PAGES) return [];\n // section -> template -> pages carrying it\n const bySection = new Map<string, Map<string, Set<string>>>();\n for (const page of pages) {\n for (const line of page.copy!.lines) {\n if (NOT_TEMPLATED.has(line.section) || line.text.split(/\\s+/).length < L4_MIN_TEMPLATE_WORDS) continue;\n if (!namesPlace(line.text, page.place!.slug, geography)) continue;\n const templates = bySection.get(line.section) ?? new Map<string, Set<string>>();\n const key = ownTemplate(line.text, page, geography);\n templates.set(key, (templates.get(key) ?? new Set()).add(page.path));\n bySection.set(line.section, templates);\n }\n }\n const regionByPath = new Map(pages.map((page) => [page.path, regionOf(page, geography)]));\n const findings: SiteSystemFinding[] = [];\n for (const [section, templates] of bySection) {\n const shared = [...templates]\n .filter(([, carriers]) => carriers.size >= Math.max(2, pages.length * L4_ROTATION_SHARE))\n .map(([key, carriers]) => ({\n key,\n carriers,\n regions: new Set([...carriers].map((path) => regionByPath.get(path))),\n others: geography.placesIn(key).sort().join(\" \"),\n }));\n let rotated: [Set<string>, Set<string>] | undefined;\n for (let left = 0; left < shared.length && !rotated; left += 1) {\n for (let right = left + 1; right < shared.length && !rotated; right += 1) {\n const a = shared[left]!;\n const b = shared[right]!;\n if (a.others !== b.others || sharedFrame(a.key, b.key) >= L4_SLOT_SHARE || [...a.carriers].some((path) => b.carriers.has(path))) continue;\n if ([...a.regions].some((region) => b.regions.has(region))) rotated = [a.carriers, b.carriers];\n }\n }\n if (!rotated) continue;\n const [first, second] = rotated;\n const example = [...first][0]!;\n const other = [...second].find((path) => regionByPath.get(path) === regionByPath.get(example)) ?? [...second][0];\n findings.push(fail(\n \"L4\",\n example,\n `The ${stem} pages rotate local copy by address in their ${section === \"page\" ? \"content\" : `\"${section}\" section`}: different templates of the same sentence with the suburb swapped in, for example on ${example} and ${other}.`,\n ));\n }\n return findings;\n}\n\nfunction faqFindings(pages: readonly EstatePage[], geography: FullGeography): SiteSystemFinding[] {\n const carriers = new Map<string, { question: string; paths: string[]; areas: Set<string> }>();\n for (const page of pages) {\n const area = page.place!.kind === \"suburb\" ? (geography.suburbs.get(page.place!.slug)?.council ?? regionOf(page, geography)) : page.place!.slug;\n for (const faq of page.copy!.faqs) {\n const text = `${faq.question} ${faq.answer}`;\n if (!namesPlace(text, page.place!.slug, geography)) continue;\n const key = geography.mask(text.toLowerCase()).replace(/\\s+/g, \" \").trim();\n const entry = carriers.get(key) ?? { question: faq.question, paths: [], areas: new Set<string>() };\n if (!entry.paths.includes(page.path)) entry.paths.push(page.path);\n entry.areas.add(area);\n carriers.set(key, entry);\n }\n }\n return [...carriers.values()]\n .filter((entry) => entry.paths.length >= 2 && entry.areas.size >= 2)\n .map((entry) => fail(\n \"L4\",\n entry.paths[0]!,\n `The FAQ \"${entry.question}\" on ${entry.paths[0]} is name-swapped: the same question and answer appear on ${plural(entry.paths.length - 1, \"other page\")} (such as ${entry.paths[1]}) with the place name changed.`,\n ));\n}\n\nfunction jobFindings(page: EstatePage, geography: FullGeography): SiteSystemFinding[] {\n const findings: SiteSystemFinding[] = [];\n for (const card of page.copy!.jobs ?? []) {\n if (/\\bnearby\\b/i.test(card)) continue;\n const other = geography.placesIn(card).find((slug) => slug !== page.place!.slug && geography.suburbs.has(slug));\n if (other) {\n findings.push(fail(\"L4\", page.path, `${page.path} shows a job in ${placeName(other, geography)} without the \"nearby\" label.`));\n }\n }\n return findings;\n}\n\nexport function checkL4(context: FullContext): SiteSystemFinding[] {\n const geography = context.geography;\n if (!geography) return [fail(\"L4\", undefined, `The place names could not be read (${context.geographyProblem}), so local copy could not be compared.`)];\n const groups = siblings(context.estate.pages);\n if (groups.size === 0) return [notApplicable(\"L4\", \"No location page with a known place was read, so there is no local copy to compare.\")];\n const failures: SiteSystemFinding[] = [];\n let pages = 0;\n for (const [key, family] of groups) {\n pages += family.length;\n failures.push(...rotationFindings(key.split(\"|\")[0]!, family, geography), ...faqFindings(family, geography));\n for (const page of family) failures.push(...jobFindings(page, geography));\n }\n return outcome(\n \"L4\",\n failures,\n `Local copy on ${plural(pages, \"location page\")} in ${plural(new Set([...groups.keys()].map((key) => key.split(\"|\")[0])).size, \"family\", \"families\")} is not rotated by address, no FAQ is name-swapped and every other suburb's job is labelled nearby.`,\n );\n}\n"]}
1
+ {"version":3,"file":"l4-local-copy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l4-local-copy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L4, local copy that is really local (Site System v2 §4.3 item 5, §4.4 step 4), compared across the sibling pages of\n * each family (pages sharing a stem such as `/services/decking/{place}`):\n * - no copy rotated by page address: one section carrying two or more templates of a local sentence (the page's own\n * suburb swapped in) that never appear together, each on two or more pages and a tenth of the family, that name\n * the same other places, alternate inside one region and are worded differently. Variants keyed to an area, or one\n * slot filled from data (\"about 5 km\" against \"just down the road\"), are data, not rotation (L10 catches distance\n * wording);\n * - no name-swapped FAQs: the same question and answer, naming the page's own place, on pages in two or more councils\n * (or regions) with only the place names changed; an answer drawn from council data stays the same within its\n * council, which is data, not a name swap;\n * - no other suburb's job without the \"nearby\" label in the nearby-jobs section. The job's suburb is the card's\n * `data-suburb` (NearbyJobs); a card without it is read for place names, never a species such as \"Sydney blue gum\".\n * A local line is one that names the page's own place; lines of 12 words or more are compared with the page's own\n * place and every number masked (FAQs with every place name and postcode masked). Reviews and job cards are left out\n * of the template comparison.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type EstatePage } from \"./estate.js\";\nimport { placeName, withoutCompoundNames, type FullGeography } from \"./geography.js\";\n\nexport const L4_MIN_TEMPLATE_WORDS = 12;\n/** Two templates sharing this much of their wording at the start and end are one sentence with a data slot. */\nexport const L4_SLOT_SHARE = 0.6;\n/** Each rotated template is on at least this share of its family's pages (hash rotation spreads them evenly). */\nexport const L4_ROTATION_SHARE = 0.1;\n/** A family needs this many pages before rotation can be told from coincidence. */\nexport const L4_MIN_ROTATION_PAGES = 4;\nconst NOT_TEMPLATED = new Set([\"faqs\", \"nearby-jobs\", \"reviews\"]);\n\nfunction namesPlace(text: string, slug: string, geography: FullGeography): boolean {\n return geography.placesIn(text).includes(slug);\n}\n\n/** A line with only the page's own place and the numbers masked, so lines keyed to another area stay apart. */\nfunction ownTemplate(text: string, page: EstatePage, geography: FullGeography): string {\n const name = placeName(page.place!.slug, geography).toLowerCase().replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\s+/g, \"\\\\s+\");\n return text\n .toLowerCase()\n .replace(new RegExp(`\\\\b(?:the\\\\s+)?${name}\\\\b`, \"g\"), \"{place}\")\n .replace(/\\d+(?:[.,]\\d+)*/g, \"#\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/** The share of the shorter template's words that both templates open and close with. */\nfunction sharedFrame(left: string, right: string): number {\n const a = left.split(\" \");\n const b = right.split(\" \");\n const shortest = Math.min(a.length, b.length);\n let start = 0;\n while (start < shortest && a[start] === b[start]) start += 1;\n let end = 0;\n while (end < shortest - start && a[a.length - 1 - end] === b[b.length - 1 - end]) end += 1;\n return shortest === 0 ? 1 : (start + end) / shortest;\n}\n\nfunction regionOf(page: EstatePage, geography: FullGeography): string {\n const place = page.place!;\n return place.kind === \"region\" ? place.slug : (geography.suburbs.get(place.slug)?.region ?? place.slug);\n}\n\nfunction siblings(pages: readonly EstatePage[]): Map<string, EstatePage[]> {\n const groups = new Map<string, EstatePage[]>();\n for (const page of pages) {\n if (!isLocationFamily(page.family) || !page.place || !page.copy) continue;\n // Suburb pages compare with suburb pages, hubs with hubs.\n const key = `${page.place.stem}|${page.place.kind}`;\n groups.set(key, [...(groups.get(key) ?? []), page]);\n }\n return groups;\n}\n\nfunction rotationFindings(stem: string, pages: readonly EstatePage[], geography: FullGeography): SiteSystemFinding[] {\n if (pages.length < L4_MIN_ROTATION_PAGES) return [];\n // section -> template -> pages carrying it\n const bySection = new Map<string, Map<string, Set<string>>>();\n for (const page of pages) {\n for (const line of page.copy!.lines) {\n if (NOT_TEMPLATED.has(line.section) || line.text.split(/\\s+/).length < L4_MIN_TEMPLATE_WORDS) continue;\n if (!namesPlace(line.text, page.place!.slug, geography)) continue;\n const templates = bySection.get(line.section) ?? new Map<string, Set<string>>();\n const key = ownTemplate(line.text, page, geography);\n templates.set(key, (templates.get(key) ?? new Set()).add(page.path));\n bySection.set(line.section, templates);\n }\n }\n const regionByPath = new Map(pages.map((page) => [page.path, regionOf(page, geography)]));\n const findings: SiteSystemFinding[] = [];\n for (const [section, templates] of bySection) {\n const shared = [...templates]\n .filter(([, carriers]) => carriers.size >= Math.max(2, pages.length * L4_ROTATION_SHARE))\n .map(([key, carriers]) => ({\n key,\n carriers,\n regions: new Set([...carriers].map((path) => regionByPath.get(path))),\n others: geography.placesIn(key).sort().join(\" \"),\n }));\n let rotated: [Set<string>, Set<string>] | undefined;\n for (let left = 0; left < shared.length && !rotated; left += 1) {\n for (let right = left + 1; right < shared.length && !rotated; right += 1) {\n const a = shared[left]!;\n const b = shared[right]!;\n if (a.others !== b.others || sharedFrame(a.key, b.key) >= L4_SLOT_SHARE || [...a.carriers].some((path) => b.carriers.has(path))) continue;\n if ([...a.regions].some((region) => b.regions.has(region))) rotated = [a.carriers, b.carriers];\n }\n }\n if (!rotated) continue;\n const [first, second] = rotated;\n const example = [...first][0]!;\n const other = [...second].find((path) => regionByPath.get(path) === regionByPath.get(example)) ?? [...second][0];\n findings.push(fail(\n \"L4\",\n example,\n `The ${stem} pages rotate local copy by address in their ${section === \"page\" ? \"content\" : `\"${section}\" section`}: different templates of the same sentence with the suburb swapped in, for example on ${example} and ${other}.`,\n ));\n }\n return findings;\n}\n\nfunction faqFindings(pages: readonly EstatePage[], geography: FullGeography): SiteSystemFinding[] {\n const carriers = new Map<string, { question: string; paths: string[]; areas: Set<string> }>();\n for (const page of pages) {\n const area = page.place!.kind === \"suburb\" ? (geography.suburbs.get(page.place!.slug)?.council ?? regionOf(page, geography)) : page.place!.slug;\n for (const faq of page.copy!.faqs) {\n const text = `${faq.question} ${faq.answer}`;\n if (!namesPlace(text, page.place!.slug, geography)) continue;\n const key = geography.mask(text.toLowerCase()).replace(/\\s+/g, \" \").trim();\n const entry = carriers.get(key) ?? { question: faq.question, paths: [], areas: new Set<string>() };\n if (!entry.paths.includes(page.path)) entry.paths.push(page.path);\n entry.areas.add(area);\n carriers.set(key, entry);\n }\n }\n return [...carriers.values()]\n .filter((entry) => entry.paths.length >= 2 && entry.areas.size >= 2)\n .map((entry) => fail(\n \"L4\",\n entry.paths[0]!,\n `The FAQ \"${entry.question}\" on ${entry.paths[0]} is name-swapped: the same question and answer appear on ${plural(entry.paths.length - 1, \"other page\")} (such as ${entry.paths[1]}) with the place name changed.`,\n ));\n}\n\n/**\n * A job's place comes from the card's structured data (`data-suburb`, the job log's suburb). Only a card without it\n * is read for place names, with place-named species (\"Sydney blue gum\") masked first.\n */\nfunction jobFindings(page: EstatePage, geography: FullGeography): SiteSystemFinding[] {\n const findings: SiteSystemFinding[] = [];\n for (const card of page.copy!.jobs ?? []) {\n if (/\\bnearby\\b/i.test(card.text)) continue;\n const places = card.place !== undefined ? [card.place] : geography.placesIn(withoutCompoundNames(card.text));\n const other = places.find((slug) => slug !== page.place!.slug && geography.suburbs.has(slug));\n if (other) {\n findings.push(fail(\"L4\", page.path, `${page.path} shows a job in ${placeName(other, geography)} without the \"nearby\" label.`));\n }\n }\n return findings;\n}\n\nexport function checkL4(context: FullContext): SiteSystemFinding[] {\n const geography = context.geography;\n if (!geography) return [fail(\"L4\", undefined, `The place names could not be read (${context.geographyProblem}), so local copy could not be compared.`)];\n const groups = siblings(context.estate.pages);\n if (groups.size === 0) return [notApplicable(\"L4\", \"No location page with a known place was read, so there is no local copy to compare.\")];\n const failures: SiteSystemFinding[] = [];\n let pages = 0;\n for (const [key, family] of groups) {\n pages += family.length;\n failures.push(...rotationFindings(key.split(\"|\")[0]!, family, geography), ...faqFindings(family, geography));\n for (const page of family) failures.push(...jobFindings(page, geography));\n }\n return outcome(\n \"L4\",\n failures,\n `Local copy on ${plural(pages, \"location page\")} in ${plural(new Set([...groups.keys()].map((key) => key.split(\"|\")[0])).size, \"family\", \"families\")} is not rotated by address, no FAQ is name-swapped and every other suburb's job is labelled nearby.`,\n );\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { dollarFigures } from "../enquiry/common.js";
2
2
  import { fail, listOf, notApplicable, outcome, plural, warning } from "./common.js";
3
3
  import { isDataBackedReason } from "./k12-manifests.js";
4
- import { placeName } from "./geography.js";
4
+ import { placeName, withoutCompoundNames } from "./geography.js";
5
5
  /** A cost-guide link in the page content (its link lists and sections), never the header, footer or navigation. */
6
6
  function linksToCostGuide(page, context) {
7
7
  return page.linkGroups.flat().some((target) => context.estate.byPath.get(target)?.family === "cost-guide" || /(?:^|[/-])cost(?:s|-guide)?(?:[/-]|$)/.test(target));
@@ -46,7 +46,7 @@ export function contractFindings(page, context) {
46
46
  if (copy.jobs.length === 0) {
47
47
  findings.push(fail("L9", page.path, `${page.path} renders its nearby-jobs section with no job in it; with no job close enough the section is left out (D2).`));
48
48
  }
49
- else if (copy.jobs.some((card) => geography.placesIn(card).length === 0)) {
49
+ else if (copy.jobs.some((card) => geography.placesIn(withoutCompoundNames(card.text)).length === 0)) {
50
50
  findings.push(fail("L9", page.path, `${page.path} shows a nearby job that does not name its suburb (D2).`));
51
51
  }
52
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"l9-data-contract.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l9-data-contract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L9, the local data contract on every service-in-suburb page (Site System v2 §4.4 step 4):\n * - D3: typical prices with a dollar figure, or a link to the cost guide while the family has no renderable price;\n * - D4: the reviews band with two or more reviews, or its fallback (Google's rating and the profile link beside what\n * it has). A profile with nothing to show, recorded in the manifest as a data reason, is a warning: a thin profile is\n * visible, never a silent failure and never a reason to hold pages;\n * - D11: the quote path on the page itself: the quote form with the suburb prefilled, a phone link and the call bar;\n * - D2: the nearby-job card or none: a nearby-jobs section that renders shows jobs that name their suburb.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { dollarFigures } from \"../enquiry/common.js\";\nimport { fail, listOf, notApplicable, outcome, plural, warning, type FullContext } from \"./common.js\";\nimport type { EstatePage } from \"./estate.js\";\nimport { isDataBackedReason } from \"./k12-manifests.js\";\nimport { placeName } from \"./geography.js\";\n\n/** A cost-guide link in the page content (its link lists and sections), never the header, footer or navigation. */\nfunction linksToCostGuide(page: EstatePage, context: FullContext): boolean {\n return page.linkGroups.flat().some((target) => context.estate.byPath.get(target)?.family === \"cost-guide\" || /(?:^|[/-])cost(?:s|-guide)?(?:[/-]|$)/.test(target));\n}\n\nexport function contractFindings(page: EstatePage, context: FullContext): SiteSystemFinding[] {\n const copy = page.copy!;\n const findings: SiteSystemFinding[] = [];\n const place = page.place && context.geography ? placeName(page.place.slug, context.geography) : undefined;\n\n if (!(copy.prices !== null && dollarFigures(copy.prices).length > 0) && !linksToCostGuide(page, context)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows no typical price and no link to the cost guide (D3).`));\n }\n\n const reviews = copy.reviews;\n if (!reviews) {\n const skip = page.manifest.kind === \"one\" ? page.manifest.manifest.skipped.find((entry) => entry.id === \"reviews\") : undefined;\n if (skip && isDataBackedReason(skip.reason)) {\n findings.push(warning(\"L9\", page.path, `${page.path} has no Google reviews or rating to show yet (${skip.reason}); the band and its fallback appear once the profile has them (D4).`));\n } else {\n findings.push(fail(\"L9\", page.path, `${page.path} shows neither reviews nor the fallback with Google's rating, count and link (D4).`));\n }\n } else if (reviews.written < 2 && !(reviews.googleLink && reviews.rating)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows ${plural(reviews.written, \"review\")} without Google's rating and profile link beside them (D4 fallback).`));\n }\n\n const missing: string[] = [];\n if (!copy.quoteForm) missing.push(\"a quote form\");\n else if (!copy.quoteForm.suburbField) missing.push(\"a suburb field in the quote form\");\n else if (!copy.quoteForm.suburbValue) missing.push(`the suburb prefilled${place ? ` as ${place}` : \"\"}`);\n else if (place && copy.quoteForm.suburbValue.toLowerCase() !== place.toLowerCase()) missing.push(`the suburb prefilled as ${place} (it says \"${copy.quoteForm.suburbValue}\")`);\n if (!copy.telLink) missing.push(\"a phone link\");\n if (!copy.callBar) missing.push(\"the call bar\");\n if (missing.length > 0) findings.push(fail(\"L9\", page.path, `${page.path} is missing part of its quote path: ${listOf(missing)} (D11).`));\n\n if (copy.jobs !== null && context.geography) {\n const geography = context.geography;\n if (copy.jobs.length === 0) {\n findings.push(fail(\"L9\", page.path, `${page.path} renders its nearby-jobs section with no job in it; with no job close enough the section is left out (D2).`));\n } else if (copy.jobs.some((card) => geography.placesIn(card).length === 0)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows a nearby job that does not name its suburb (D2).`));\n }\n }\n return findings;\n}\n\nexport function checkL9(context: FullContext): SiteSystemFinding[] {\n const pages = context.estate.pages.filter((page) => page.family === \"service-in-suburb\" && page.copy);\n if (pages.length === 0) return [notApplicable(\"L9\", \"No service-in-suburb page was read.\")];\n const findings = pages.flatMap((page) => contractFindings(page, context));\n if (!context.geography) {\n findings.push(fail(\"L9\", undefined, `The place names could not be read (${context.geographyProblem}), so the suburb prefill and the nearby-job cards could not be checked.`));\n }\n return outcome(\"L9\", findings, `All ${plural(pages.length, \"service-in-suburb page\")} carry typical prices or the cost-guide link, reviews or their fallback, and the full quote path.`);\n}\n"]}
1
+ {"version":3,"file":"l9-data-contract.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l9-data-contract.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * L9, the local data contract on every service-in-suburb page (Site System v2 §4.4 step 4):\n * - D3: typical prices with a dollar figure, or a link to the cost guide while the family has no renderable price;\n * - D4: the reviews band with two or more reviews, or its fallback (Google's rating and the profile link beside what\n * it has). A profile with nothing to show, recorded in the manifest as a data reason, is a warning: a thin profile is\n * visible, never a silent failure and never a reason to hold pages;\n * - D11: the quote path on the page itself: the quote form with the suburb prefilled, a phone link and the call bar;\n * - D2: the nearby-job card or none: a nearby-jobs section that renders shows jobs that name their suburb.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { dollarFigures } from \"../enquiry/common.js\";\nimport { fail, listOf, notApplicable, outcome, plural, warning, type FullContext } from \"./common.js\";\nimport type { EstatePage } from \"./estate.js\";\nimport { isDataBackedReason } from \"./k12-manifests.js\";\nimport { placeName, withoutCompoundNames } from \"./geography.js\";\n\n/** A cost-guide link in the page content (its link lists and sections), never the header, footer or navigation. */\nfunction linksToCostGuide(page: EstatePage, context: FullContext): boolean {\n return page.linkGroups.flat().some((target) => context.estate.byPath.get(target)?.family === \"cost-guide\" || /(?:^|[/-])cost(?:s|-guide)?(?:[/-]|$)/.test(target));\n}\n\nexport function contractFindings(page: EstatePage, context: FullContext): SiteSystemFinding[] {\n const copy = page.copy!;\n const findings: SiteSystemFinding[] = [];\n const place = page.place && context.geography ? placeName(page.place.slug, context.geography) : undefined;\n\n if (!(copy.prices !== null && dollarFigures(copy.prices).length > 0) && !linksToCostGuide(page, context)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows no typical price and no link to the cost guide (D3).`));\n }\n\n const reviews = copy.reviews;\n if (!reviews) {\n const skip = page.manifest.kind === \"one\" ? page.manifest.manifest.skipped.find((entry) => entry.id === \"reviews\") : undefined;\n if (skip && isDataBackedReason(skip.reason)) {\n findings.push(warning(\"L9\", page.path, `${page.path} has no Google reviews or rating to show yet (${skip.reason}); the band and its fallback appear once the profile has them (D4).`));\n } else {\n findings.push(fail(\"L9\", page.path, `${page.path} shows neither reviews nor the fallback with Google's rating, count and link (D4).`));\n }\n } else if (reviews.written < 2 && !(reviews.googleLink && reviews.rating)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows ${plural(reviews.written, \"review\")} without Google's rating and profile link beside them (D4 fallback).`));\n }\n\n const missing: string[] = [];\n if (!copy.quoteForm) missing.push(\"a quote form\");\n else if (!copy.quoteForm.suburbField) missing.push(\"a suburb field in the quote form\");\n else if (!copy.quoteForm.suburbValue) missing.push(`the suburb prefilled${place ? ` as ${place}` : \"\"}`);\n else if (place && copy.quoteForm.suburbValue.toLowerCase() !== place.toLowerCase()) missing.push(`the suburb prefilled as ${place} (it says \"${copy.quoteForm.suburbValue}\")`);\n if (!copy.telLink) missing.push(\"a phone link\");\n if (!copy.callBar) missing.push(\"the call bar\");\n if (missing.length > 0) findings.push(fail(\"L9\", page.path, `${page.path} is missing part of its quote path: ${listOf(missing)} (D11).`));\n\n if (copy.jobs !== null && context.geography) {\n const geography = context.geography;\n if (copy.jobs.length === 0) {\n findings.push(fail(\"L9\", page.path, `${page.path} renders its nearby-jobs section with no job in it; with no job close enough the section is left out (D2).`));\n } else if (copy.jobs.some((card) => geography.placesIn(withoutCompoundNames(card.text)).length === 0)) {\n findings.push(fail(\"L9\", page.path, `${page.path} shows a nearby job that does not name its suburb (D2).`));\n }\n }\n return findings;\n}\n\nexport function checkL9(context: FullContext): SiteSystemFinding[] {\n const pages = context.estate.pages.filter((page) => page.family === \"service-in-suburb\" && page.copy);\n if (pages.length === 0) return [notApplicable(\"L9\", \"No service-in-suburb page was read.\")];\n const findings = pages.flatMap((page) => contractFindings(page, context));\n if (!context.geography) {\n findings.push(fail(\"L9\", undefined, `The place names could not be read (${context.geographyProblem}), so the suburb prefill and the nearby-job cards could not be checked.`));\n }\n return outcome(\"L9\", findings, `All ${plural(pages.length, \"service-in-suburb page\")} carry typical prices or the cost-guide link, reviews or their fallback, and the full quote path.`);\n}\n"]}
@@ -2,7 +2,9 @@
2
2
  * The link graph of the built estate (Site System v2 §4.4 step 7), computed from the route catalogue as built and the
3
3
  * links in every page's server HTML, never by crawling:
4
4
  * - E5: every indexable page but home has at least one link from another page;
5
- * - L6: each indexable suburb and service-in-suburb page has 6 or more inbound links from 3 or more page types;
5
+ * - L6: each indexable suburb and service-in-suburb page has 6 or more inbound links from 3 or more page types (a
6
+ * linking page's type is its manifest family, else its catalogue hub kind as `locations-hub` or `services-hub`, else
7
+ * the family the estate inferred);
6
8
  * - L7: each indexable location page is 3 clicks or fewer from home;
7
9
  * - L8: each family of suburb pages has a hub in every region it serves (`{stem}` with the region, or `{region}-area`).
8
10
  */
@@ -1 +1 @@
1
- {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/links.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACzG,OAAO,EAAoB,KAAK,MAAM,EAAmB,MAAM,aAAa,CAAC;AAG7E,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAMrE;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB3E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,iBAAiB,EAAE,CAapH;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,iBAAiB,EAAE,CAkBpH;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAcjE;AAOD,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAmCjE"}
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/links.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACzG,OAAO,EAAoB,KAAK,MAAM,EAAmB,MAAM,aAAa,CAAC;AAI7E,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAMrE;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB3E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,iBAAiB,EAAE,CAapH;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,iBAAiB,EAAE,CAuBpH;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAcjE;AAWD,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAmCjE"}
@@ -1,6 +1,7 @@
1
1
  import { fail, listOf, needsBuild, notApplicable, outcome, plural } from "./common.js";
2
2
  import { isLocationFamily } from "./estate.js";
3
3
  import { placeName } from "./geography.js";
4
+ import { linkingPageType } from "./route-kinds.js";
4
5
  export const L6_MIN_INBOUND = 6;
5
6
  export const L6_MIN_PAGE_TYPES = 3;
6
7
  export const L7_MAX_DEPTH = 3;
@@ -60,7 +61,12 @@ export function checkL6(context, inbound) {
60
61
  return [notApplicable("L6", "The build has no indexable suburb or service-in-suburb pages.")];
61
62
  for (const page of pages) {
62
63
  const sources = [...(inbound.get(page.path) ?? [])];
63
- const types = [...new Set(sources.flatMap((source) => context.estate.byPath.get(source)?.family ?? []))].sort();
64
+ // A hub without a section manifest is typed by its catalogue kind (AA's /locations, the location-index).
65
+ const kinds = context.catalogueKinds ?? new Map();
66
+ const types = [...new Set(sources.flatMap((source) => {
67
+ const linking = context.estate.byPath.get(source);
68
+ return (linking ? linkingPageType(linking, kinds) : undefined) ?? [];
69
+ }))].sort();
64
70
  if (sources.length < L6_MIN_INBOUND || types.length < L6_MIN_PAGE_TYPES) {
65
71
  failures.push(fail("L6", page.path, `${page.path} has ${plural(sources.length, "inbound link")} from ${plural(types.length, "page type")}${types.length > 0 ? ` (${listOf(types)})` : ""}; a location page needs ${L6_MIN_INBOUND} or more from ${L6_MIN_PAGE_TYPES} or more, such as its hub, suburb page, neighbours and service page.`));
66
72
  }
@@ -87,9 +93,13 @@ export function checkL7(context) {
87
93
  }
88
94
  return outcome("L7", failures, `All ${plural(pages.length, "location page")} are ${L7_MAX_DEPTH} clicks or fewer from home.`);
89
95
  }
90
- /** The hub path for a stem and region: `{stem}` with the region slug, or with `{region}-area`. */
91
- function hubCandidates(stem, region) {
92
- return [stem.replace("{place}", region), stem.replace("{place}", `${region}-area`)];
96
+ /**
97
+ * The hub path for a stem and region: `{stem}` with the public slug the estate plan gives the region
98
+ * (`estate.slugs.regions`, as AA's `sydney-city` → `sydney-cbd`), then with the region slug, then `{region}-area`.
99
+ */
100
+ function hubCandidates(stem, region, renames) {
101
+ const renamed = renames && Object.hasOwn(renames, region) ? renames[region] : undefined;
102
+ return [...new Set([...(renamed ? [stem.replace("{place}", renamed)] : []), stem.replace("{place}", region), stem.replace("{place}", `${region}-area`)])];
93
103
  }
94
104
  export function checkL8(context) {
95
105
  const skip = needsBuild("L8", context.estate);
@@ -115,7 +125,7 @@ export function checkL8(context) {
115
125
  let hubs = 0;
116
126
  for (const [stem, regions] of families) {
117
127
  for (const [region, pages] of regions) {
118
- const candidates = hubCandidates(stem, region);
128
+ const candidates = hubCandidates(stem, region, context.strategy.estate?.slugs?.regions);
119
129
  const hub = candidates.map((path) => context.estate.byPath.get(path)).find((page) => page !== undefined);
120
130
  if (hub?.indexable) {
121
131
  hubs += 1;