@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":"links.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/links.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The link graph of the built estate (Site System v2 §4.4 step 7), computed from the route catalogue as built and the\n * links in every page's server HTML, never by crawling:\n * - E5: every indexable page but home has at least one link from another page;\n * - L6: each indexable suburb and service-in-suburb page has 6 or more inbound links from 3 or more page types;\n * - L7: each indexable location page is 3 clicks or fewer from home;\n * - L8: each family of suburb pages has a hub in every region it serves (`{stem}` with the region, or `{region}-area`).\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, listOf, needsBuild, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type Estate, type EstatePage } from \"./estate.js\";\nimport { placeName } from \"./geography.js\";\n\nexport const L6_MIN_INBOUND = 6;\nexport const L6_MIN_PAGE_TYPES = 3;\nexport const L7_MAX_DEPTH = 3;\n\n/** For each page, the other pages that link to it. */\nexport function inboundLinks(estate: Estate): Map<string, Set<string>> {\n const inbound = new Map<string, Set<string>>(estate.pages.map((page) => [page.path, new Set<string>()]));\n for (const page of estate.pages) {\n for (const target of page.links) inbound.get(target)?.add(page.path);\n }\n return inbound;\n}\n\n/** Clicks from home to every page it reaches. */\nexport function clickDepths(estate: Estate, home = \"/\"): Map<string, number> {\n const depths = new Map<string, number>();\n if (!estate.byPath.has(home)) return depths;\n depths.set(home, 0);\n let frontier = [home];\n while (frontier.length > 0) {\n const next: string[] = [];\n for (const path of frontier) {\n const depth = depths.get(path)!;\n for (const target of estate.byPath.get(path)?.links ?? []) {\n if (!estate.byPath.has(target) || depths.has(target)) continue;\n depths.set(target, depth + 1);\n next.push(target);\n }\n }\n frontier = next;\n }\n return depths;\n}\n\nexport function checkE5(context: FullContext, inbound: ReadonlyMap<string, ReadonlySet<string>>): SiteSystemFinding[] {\n const skip = needsBuild(\"E5\", context.estate);\n if (skip) return [skip];\n const failures: SiteSystemFinding[] = context.estate.missingHtml\n .filter((item) => !item.location)\n .map((item) => fail(\"E5\", item.path, `${item.path} is a prerendered route with no HTML in the build, so nothing can link to it.`));\n const indexable = context.estate.pages.filter((page) => page.indexable && page.path !== \"/\");\n for (const page of indexable) {\n if ((inbound.get(page.path)?.size ?? 0) === 0) {\n failures.push(fail(\"E5\", page.path, `${page.path} is indexable but no other page links to it.`));\n }\n }\n return outcome(\"E5\", failures, `Every one of ${plural(indexable.length, \"indexable page\")} other than home has a link from another page.`);\n}\n\nexport function checkL6(context: FullContext, inbound: ReadonlyMap<string, ReadonlySet<string>>): SiteSystemFinding[] {\n const skip = needsBuild(\"L6\", context.estate);\n if (skip) return [skip];\n const failures: SiteSystemFinding[] = [];\n const pages = context.estate.pages.filter((page) => page.indexable && (page.family === \"suburb\" || page.family === \"service-in-suburb\"));\n if (pages.length === 0) return [notApplicable(\"L6\", \"The build has no indexable suburb or service-in-suburb pages.\")];\n for (const page of pages) {\n const sources = [...(inbound.get(page.path) ?? [])];\n const types = [...new Set(sources.flatMap((source) => context.estate.byPath.get(source)?.family ?? []))].sort();\n if (sources.length < L6_MIN_INBOUND || types.length < L6_MIN_PAGE_TYPES) {\n failures.push(fail(\n \"L6\",\n page.path,\n `${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.`,\n ));\n }\n }\n return outcome(\"L6\", failures, `All ${plural(pages.length, \"suburb and service-in-suburb page\")} have ${L6_MIN_INBOUND} or more inbound links from ${L6_MIN_PAGE_TYPES} or more page types.`);\n}\n\nexport function checkL7(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"L7\", context.estate);\n if (skip) return [skip];\n const pages = context.estate.pages.filter((page) => page.indexable && isLocationFamily(page.family));\n if (pages.length === 0) return [notApplicable(\"L7\", \"The build has no indexable location pages.\")];\n if (!context.estate.byPath.has(\"/\")) return [fail(\"L7\", \"/\", \"The build has no home page, so click depth cannot be measured.\")];\n const depths = clickDepths(context.estate);\n const failures: SiteSystemFinding[] = [];\n for (const page of pages) {\n const depth = depths.get(page.path);\n if (depth === undefined) failures.push(fail(\"L7\", page.path, `${page.path} cannot be reached from home by following links.`));\n else if (depth > L7_MAX_DEPTH) failures.push(fail(\"L7\", page.path, `${page.path} is ${depth} clicks from home; location pages are ${L7_MAX_DEPTH} or fewer.`));\n }\n return outcome(\"L7\", failures, `All ${plural(pages.length, \"location page\")} are ${L7_MAX_DEPTH} clicks or fewer from home.`);\n}\n\n/** The hub path for a stem and region: `{stem}` with the region slug, or with `{region}-area`. */\nfunction hubCandidates(stem: string, region: string): string[] {\n return [stem.replace(\"{place}\", region), stem.replace(\"{place}\", `${region}-area`)];\n}\n\nexport function checkL8(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"L8\", context.estate);\n if (skip) return [skip];\n const geography = context.geography;\n if (!geography) return [fail(\"L8\", undefined, `The regions of the suburbs could not be read (${context.geographyProblem}), so hubs could not be checked.`)];\n const families = new Map<string, Map<string, EstatePage[]>>();\n for (const page of context.estate.pages) {\n if (!page.indexable || (page.family !== \"suburb\" && page.family !== \"service-in-suburb\") || page.place?.kind !== \"suburb\") continue;\n const region = geography.suburbs.get(page.place.slug)?.region;\n if (!region) continue;\n const regions = families.get(page.place.stem) ?? new Map<string, EstatePage[]>();\n regions.set(region, [...(regions.get(region) ?? []), page]);\n families.set(page.place.stem, regions);\n }\n if (families.size === 0) return [notApplicable(\"L8\", \"The build has no indexable suburb or service-in-suburb pages in registry suburbs.\")];\n const failures: SiteSystemFinding[] = [];\n let hubs = 0;\n for (const [stem, regions] of families) {\n for (const [region, pages] of regions) {\n const candidates = hubCandidates(stem, region);\n const hub = candidates.map((path) => context.estate.byPath.get(path)).find((page) => page !== undefined);\n if (hub?.indexable) {\n hubs += 1;\n continue;\n }\n failures.push(fail(\n \"L8\",\n candidates[0],\n hub\n ? `The hub ${hub.path} for the ${stem} pages in ${placeName(region, geography)} is noindex; every family has an indexable hub in each region it serves.`\n : `The ${stem} pages have ${plural(pages.length, \"page\")} in ${placeName(region, geography)} but no hub for that region (${candidates[0]}).`,\n ));\n }\n }\n return outcome(\"L8\", failures, `Every family of location pages has a hub in each region it serves (${plural(hubs, \"hub\")} across ${plural(families.size, \"family\", \"families\")}).`);\n}\n"]}
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/links.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The link graph of the built estate (Site System v2 §4.4 step 7), computed from the route catalogue as built and the\n * links in every page's server HTML, never by crawling:\n * - E5: every indexable page but home has at least one link from another page;\n * - L6: each indexable suburb and service-in-suburb page has 6 or more inbound links from 3 or more page types (a\n * linking page's type is its manifest family, else its catalogue hub kind as `locations-hub` or `services-hub`, else\n * the family the estate inferred);\n * - L7: each indexable location page is 3 clicks or fewer from home;\n * - L8: each family of suburb pages has a hub in every region it serves (`{stem}` with the region, or `{region}-area`).\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, listOf, needsBuild, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\nimport { isLocationFamily, type Estate, type EstatePage } from \"./estate.js\";\nimport { placeName } from \"./geography.js\";\nimport { linkingPageType } from \"./route-kinds.js\";\n\nexport const L6_MIN_INBOUND = 6;\nexport const L6_MIN_PAGE_TYPES = 3;\nexport const L7_MAX_DEPTH = 3;\n\n/** For each page, the other pages that link to it. */\nexport function inboundLinks(estate: Estate): Map<string, Set<string>> {\n const inbound = new Map<string, Set<string>>(estate.pages.map((page) => [page.path, new Set<string>()]));\n for (const page of estate.pages) {\n for (const target of page.links) inbound.get(target)?.add(page.path);\n }\n return inbound;\n}\n\n/** Clicks from home to every page it reaches. */\nexport function clickDepths(estate: Estate, home = \"/\"): Map<string, number> {\n const depths = new Map<string, number>();\n if (!estate.byPath.has(home)) return depths;\n depths.set(home, 0);\n let frontier = [home];\n while (frontier.length > 0) {\n const next: string[] = [];\n for (const path of frontier) {\n const depth = depths.get(path)!;\n for (const target of estate.byPath.get(path)?.links ?? []) {\n if (!estate.byPath.has(target) || depths.has(target)) continue;\n depths.set(target, depth + 1);\n next.push(target);\n }\n }\n frontier = next;\n }\n return depths;\n}\n\nexport function checkE5(context: FullContext, inbound: ReadonlyMap<string, ReadonlySet<string>>): SiteSystemFinding[] {\n const skip = needsBuild(\"E5\", context.estate);\n if (skip) return [skip];\n const failures: SiteSystemFinding[] = context.estate.missingHtml\n .filter((item) => !item.location)\n .map((item) => fail(\"E5\", item.path, `${item.path} is a prerendered route with no HTML in the build, so nothing can link to it.`));\n const indexable = context.estate.pages.filter((page) => page.indexable && page.path !== \"/\");\n for (const page of indexable) {\n if ((inbound.get(page.path)?.size ?? 0) === 0) {\n failures.push(fail(\"E5\", page.path, `${page.path} is indexable but no other page links to it.`));\n }\n }\n return outcome(\"E5\", failures, `Every one of ${plural(indexable.length, \"indexable page\")} other than home has a link from another page.`);\n}\n\nexport function checkL6(context: FullContext, inbound: ReadonlyMap<string, ReadonlySet<string>>): SiteSystemFinding[] {\n const skip = needsBuild(\"L6\", context.estate);\n if (skip) return [skip];\n const failures: SiteSystemFinding[] = [];\n const pages = context.estate.pages.filter((page) => page.indexable && (page.family === \"suburb\" || page.family === \"service-in-suburb\"));\n if (pages.length === 0) return [notApplicable(\"L6\", \"The build has no indexable suburb or service-in-suburb pages.\")];\n for (const page of pages) {\n const sources = [...(inbound.get(page.path) ?? [])];\n // A hub without a section manifest is typed by its catalogue kind (AA's /locations, the location-index).\n const kinds = context.catalogueKinds ?? new Map<string, string>();\n const types = [...new Set(sources.flatMap((source) => {\n const linking = context.estate.byPath.get(source);\n return (linking ? linkingPageType(linking, kinds) : undefined) ?? [];\n }))].sort();\n if (sources.length < L6_MIN_INBOUND || types.length < L6_MIN_PAGE_TYPES) {\n failures.push(fail(\n \"L6\",\n page.path,\n `${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.`,\n ));\n }\n }\n return outcome(\"L6\", failures, `All ${plural(pages.length, \"suburb and service-in-suburb page\")} have ${L6_MIN_INBOUND} or more inbound links from ${L6_MIN_PAGE_TYPES} or more page types.`);\n}\n\nexport function checkL7(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"L7\", context.estate);\n if (skip) return [skip];\n const pages = context.estate.pages.filter((page) => page.indexable && isLocationFamily(page.family));\n if (pages.length === 0) return [notApplicable(\"L7\", \"The build has no indexable location pages.\")];\n if (!context.estate.byPath.has(\"/\")) return [fail(\"L7\", \"/\", \"The build has no home page, so click depth cannot be measured.\")];\n const depths = clickDepths(context.estate);\n const failures: SiteSystemFinding[] = [];\n for (const page of pages) {\n const depth = depths.get(page.path);\n if (depth === undefined) failures.push(fail(\"L7\", page.path, `${page.path} cannot be reached from home by following links.`));\n else if (depth > L7_MAX_DEPTH) failures.push(fail(\"L7\", page.path, `${page.path} is ${depth} clicks from home; location pages are ${L7_MAX_DEPTH} or fewer.`));\n }\n return outcome(\"L7\", failures, `All ${plural(pages.length, \"location page\")} are ${L7_MAX_DEPTH} clicks or fewer from home.`);\n}\n\n/**\n * The hub path for a stem and region: `{stem}` with the public slug the estate plan gives the region\n * (`estate.slugs.regions`, as AA's `sydney-city` → `sydney-cbd`), then with the region slug, then `{region}-area`.\n */\nfunction hubCandidates(stem: string, region: string, renames: Readonly<Record<string, string>> | undefined): string[] {\n const renamed = renames && Object.hasOwn(renames, region) ? renames[region] : undefined;\n return [...new Set([...(renamed ? [stem.replace(\"{place}\", renamed)] : []), stem.replace(\"{place}\", region), stem.replace(\"{place}\", `${region}-area`)])];\n}\n\nexport function checkL8(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"L8\", context.estate);\n if (skip) return [skip];\n const geography = context.geography;\n if (!geography) return [fail(\"L8\", undefined, `The regions of the suburbs could not be read (${context.geographyProblem}), so hubs could not be checked.`)];\n const families = new Map<string, Map<string, EstatePage[]>>();\n for (const page of context.estate.pages) {\n if (!page.indexable || (page.family !== \"suburb\" && page.family !== \"service-in-suburb\") || page.place?.kind !== \"suburb\") continue;\n const region = geography.suburbs.get(page.place.slug)?.region;\n if (!region) continue;\n const regions = families.get(page.place.stem) ?? new Map<string, EstatePage[]>();\n regions.set(region, [...(regions.get(region) ?? []), page]);\n families.set(page.place.stem, regions);\n }\n if (families.size === 0) return [notApplicable(\"L8\", \"The build has no indexable suburb or service-in-suburb pages in registry suburbs.\")];\n const failures: SiteSystemFinding[] = [];\n let hubs = 0;\n for (const [stem, regions] of families) {\n for (const [region, pages] of regions) {\n const candidates = hubCandidates(stem, region, context.strategy.estate?.slugs?.regions);\n const hub = candidates.map((path) => context.estate.byPath.get(path)).find((page) => page !== undefined);\n if (hub?.indexable) {\n hubs += 1;\n continue;\n }\n failures.push(fail(\n \"L8\",\n candidates[0],\n hub\n ? `The hub ${hub.path} for the ${stem} pages in ${placeName(region, geography)} is noindex; every family has an indexable hub in each region it serves.`\n : `The ${stem} pages have ${plural(pages.length, \"page\")} in ${placeName(region, geography)} but no hub for that region (${candidates[0]}).`,\n ));\n }\n }\n return outcome(\"L8\", failures, `Every family of location pages has a hub in each region it serves (${plural(hubs, \"hub\")} across ${plural(families.size, \"family\", \"families\")}).`);\n}\n"]}
@@ -5,6 +5,11 @@
5
5
  * sitemap. A wave that is due has every page it lists built, indexable and in the sitemap. The schedule itself goes
6
6
  * out in date order, 14 to 28 days apart, so it never holds a page open-ended. Nothing here suggests cutting, capping
7
7
  * or noindexing a page.
8
+ *
9
+ * From 3.14.0 an existing estate can record its plan (`estate` in strategy.ts): the pages indexed now, return waves
10
+ * (`kind: 'return'`: pages live now that come back to the index on the day, so they must be in the build already), the
11
+ * family paths and the public slugs. With a plan, every location page is indexed now or in a wave, a page indexed now
12
+ * never waits for a wave, and a region page that takes a suburb's slug sends that suburb to "{slug}-suburb".
8
13
  */
9
14
  import type { SiteSystemFinding } from "../../checks.js";
10
15
  import { type FullContext } from "./common.js";
@@ -1 +1 @@
1
- {"version":3,"file":"loc-index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/loc-index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjG,wHAAwH;AACxH,eAAO,MAAM,kBAAkB,gFAAiF,CAAC;AAGjH,0CAA0C;AAC1C,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE5C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA4C/E"}
1
+ {"version":3,"file":"loc-index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/loc-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjG,wHAAwH;AACxH,eAAO,MAAM,kBAAkB,gFAAiF,CAAC;AAGjH,0CAA0C;AAC1C,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE5C;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAgF/E"}
@@ -23,15 +23,27 @@ export function checkLocationIndexing(context) {
23
23
  if (estate.sitemap === null && pages.length > 0) {
24
24
  failures.push(fail("LOC-INDEX", undefined, "The build holds no sitemap file, so whether every location page is listed could not be checked."));
25
25
  }
26
+ const plan = strategy.estate;
27
+ const indexedNow = (path) => plan?.indexedNow.some((route) => matchesWaveRoute(path, route)) ?? false;
26
28
  let live = 0;
27
29
  let scheduled = 0;
28
30
  for (const page of pages) {
29
31
  const wave = waveOf(page.path);
30
- if (wave && wave.releaseOn > today) {
32
+ if (plan && !wave && !indexedNow(page.path) && page.family !== "locations-hub" && page.family !== "council") {
33
+ failures.push(fail("LOC-INDEX", page.path, `${page.path} is a location page the estate plan does not account for; list it in estate.indexedNow or in a wave.`));
34
+ }
35
+ const now = indexedNow(page.path);
36
+ const waiting = wave !== undefined && wave.releaseOn > today;
37
+ if (waiting && !now) {
31
38
  scheduled += 1;
32
39
  continue;
33
40
  }
34
- const dated = wave ? `its wave was due on ${wave.releaseOn}` : "no upcoming wave in the recorded schedule dates it";
41
+ // A page indexed now is judged now, even when a wave's pattern also covers it; a wave naming a page the plan's
42
+ // pattern indexes now is a contradiction (the same path in both lists is reported with the plan's problems).
43
+ if (waiting && wave.routes.includes(page.path) && !plan?.indexedNow.includes(page.path)) {
44
+ failures.push(fail("LOC-INDEX", page.path, `${page.path} is indexed now in the estate plan, but the wave of ${wave.releaseOn} still lists it; a page indexed now never waits for a wave.`));
45
+ }
46
+ const dated = waiting ? "it is indexed now in the estate plan" : wave ? `its wave was due on ${wave.releaseOn}` : "no upcoming wave in the recorded schedule dates it";
35
47
  const problems = [];
36
48
  if (!page.indexable)
37
49
  problems.push("is marked noindex");
@@ -53,6 +65,35 @@ export function checkLocationIndexing(context) {
53
65
  }
54
66
  }
55
67
  }
68
+ // A return wave brings back pages that are live now, so its pages are in the build before the day.
69
+ for (const wave of waves) {
70
+ if (wave.kind !== "return" || wave.releaseOn <= today)
71
+ continue;
72
+ for (const route of wave.routes) {
73
+ const matched = route.includes("*") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);
74
+ if (!matched)
75
+ failures.push(fail("LOC-INDEX", route, `The return wave of ${wave.releaseOn} brings back ${route}, but the build has no ${route.includes("*") ? "page matching it" : "such page"}; a return wave lists pages that are live now.`));
76
+ }
77
+ }
78
+ if (plan) {
79
+ for (const route of plan.indexedNow) {
80
+ const matched = route.includes("*") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);
81
+ if (!matched)
82
+ failures.push(fail("LOC-INDEX", route, `estate.indexedNow lists ${route}, but the build has no ${route.includes("*") ? "page matching it" : "such page"}.`));
83
+ }
84
+ const parent = (template) => template?.slice(0, template.lastIndexOf("/"));
85
+ const shared = plan.paths?.region !== undefined && parent(plan.paths.region) === parent(plan.paths.suburb);
86
+ for (const page of shared ? pages : []) {
87
+ if (page.place?.kind !== "region")
88
+ continue;
89
+ const slug = page.path.slice(page.path.lastIndexOf("/") + 1);
90
+ if (!context.geography?.suburbs.has(slug) || page.path !== `${parent(plan.paths?.region)}/${slug}`)
91
+ continue;
92
+ if (plan.slugs?.suburbs?.[slug] !== `${slug}-suburb`) {
93
+ failures.push(fail("LOC-INDEX", page.path, `The region page ${page.path} takes the slug of the suburb ${slug}; that suburb's pages take "${slug}-suburb" (estate.slugs.suburbs).`));
94
+ }
95
+ }
96
+ }
56
97
  const upcoming = scheduled > 0 ? `; ${plural(scheduled, "more page")} ${scheduled === 1 ? "is" : "are"} dated by upcoming waves` : "";
57
98
  return outcome("LOC-INDEX", failures, `All ${plural(live, "released location page")} are indexable and in the sitemap${upcoming}.`);
58
99
  }
@@ -1 +1 @@
1
- {"version":3,"file":"loc-index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/loc-index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The location indexing guard (Site System v2 §4.3 item 3, §4.4 step 8; D4, 20 Oct 2026): every location page stays\n * indexed. A suburb, service-in-suburb or region page is indexable and listed in the sitemap, unless a wave in the\n * recorded schedule (`waves` in strategy.ts) dates it in the future: until that day it may be noindex or out of the\n * sitemap. A wave that is due has every page it lists built, indexable and in the sitemap. The schedule itself goes\n * out in date order, 14 to 28 days apart, so it never holds a page open-ended. Nothing here suggests cutting, capping\n * or noindexing a page.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, needsBuild, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\n\n/** The families the guard covers: the location families, council pages and the locations hub (v2 §4.1, §4.3 item 3). */\nexport const LOC_INDEX_FAMILIES = [\"suburb\", \"service-in-suburb\", \"region\", \"council\", \"locations-hub\"] as const;\nimport { FULL_STRATEGY_FILE, matchesWaveRoute } from \"./strategy.js\";\n\n/** Today's date in Sydney, YYYY-MM-DD. */\nexport function sydneyDay(date: Date): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Australia/Sydney\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(date);\n}\n\nexport function checkLocationIndexing(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"LOC-INDEX\", context.estate);\n if (skip) return [skip];\n const { estate, strategy, today } = context;\n const failures: SiteSystemFinding[] = strategy.problems.waves.map((problem) => fail(\"LOC-INDEX\", FULL_STRATEGY_FILE, problem));\n const waves = strategy.waves;\n const waveOf = (path: string) => waves.find((wave) => wave.routes.some((route) => matchesWaveRoute(path, route)));\n const pages = estate.pages.filter((page) => (LOC_INDEX_FAMILIES as readonly string[]).includes(page.family ?? \"\"));\n for (const missing of estate.missingHtml.filter((item) => item.location)) {\n failures.push(fail(\"LOC-INDEX\", missing.path, `${missing.path} is a prerendered location route with no HTML in the build, so it cannot be served or indexed.`));\n }\n if (pages.length === 0 && waves.length === 0) return [...failures, notApplicable(\"LOC-INDEX\", \"The build has no location pages.\")];\n if (estate.sitemap === null && pages.length > 0) {\n failures.push(fail(\"LOC-INDEX\", undefined, \"The build holds no sitemap file, so whether every location page is listed could not be checked.\"));\n }\n\n let live = 0;\n let scheduled = 0;\n for (const page of pages) {\n const wave = waveOf(page.path);\n if (wave && wave.releaseOn > today) {\n scheduled += 1;\n continue;\n }\n const dated = wave ? `its wave was due on ${wave.releaseOn}` : \"no upcoming wave in the recorded schedule dates it\";\n const problems: string[] = [];\n if (!page.indexable) problems.push(\"is marked noindex\");\n if (estate.sitemap && !estate.sitemap.has(page.path)) problems.push(\"is not in the sitemap\");\n if (problems.length > 0) {\n failures.push(fail(\"LOC-INDEX\", page.path, `${page.path} is a location page that ${problems.join(\" and \")}, and ${dated}; every location page is indexable and listed.`));\n } else live += 1;\n }\n\n for (const wave of waves) {\n if (wave.releaseOn > today) continue;\n for (const route of wave.routes) {\n const matched = route.includes(\"*\") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);\n if (!matched) {\n failures.push(fail(\"LOC-INDEX\", route, `The wave of ${wave.releaseOn} released ${route}, but the build has no ${route.includes(\"*\") ? \"page matching it\" : \"such page\"}.`));\n }\n }\n }\n const upcoming = scheduled > 0 ? `; ${plural(scheduled, \"more page\")} ${scheduled === 1 ? \"is\" : \"are\"} dated by upcoming waves` : \"\";\n return outcome(\"LOC-INDEX\", failures, `All ${plural(live, \"released location page\")} are indexable and in the sitemap${upcoming}.`);\n}\n"]}
1
+ {"version":3,"file":"loc-index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/loc-index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The location indexing guard (Site System v2 §4.3 item 3, §4.4 step 8; D4, 20 Oct 2026): every location page stays\n * indexed. A suburb, service-in-suburb or region page is indexable and listed in the sitemap, unless a wave in the\n * recorded schedule (`waves` in strategy.ts) dates it in the future: until that day it may be noindex or out of the\n * sitemap. A wave that is due has every page it lists built, indexable and in the sitemap. The schedule itself goes\n * out in date order, 14 to 28 days apart, so it never holds a page open-ended. Nothing here suggests cutting, capping\n * or noindexing a page.\n *\n * From 3.14.0 an existing estate can record its plan (`estate` in strategy.ts): the pages indexed now, return waves\n * (`kind: 'return'`: pages live now that come back to the index on the day, so they must be in the build already), the\n * family paths and the public slugs. With a plan, every location page is indexed now or in a wave, a page indexed now\n * never waits for a wave, and a region page that takes a suburb's slug sends that suburb to \"{slug}-suburb\".\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, needsBuild, notApplicable, outcome, plural, type FullContext } from \"./common.js\";\n\n/** The families the guard covers: the location families, council pages and the locations hub (v2 §4.1, §4.3 item 3). */\nexport const LOC_INDEX_FAMILIES = [\"suburb\", \"service-in-suburb\", \"region\", \"council\", \"locations-hub\"] as const;\nimport { FULL_STRATEGY_FILE, matchesWaveRoute } from \"./strategy.js\";\n\n/** Today's date in Sydney, YYYY-MM-DD. */\nexport function sydneyDay(date: Date): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Australia/Sydney\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(date);\n}\n\nexport function checkLocationIndexing(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"LOC-INDEX\", context.estate);\n if (skip) return [skip];\n const { estate, strategy, today } = context;\n const failures: SiteSystemFinding[] = strategy.problems.waves.map((problem) => fail(\"LOC-INDEX\", FULL_STRATEGY_FILE, problem));\n const waves = strategy.waves;\n const waveOf = (path: string) => waves.find((wave) => wave.routes.some((route) => matchesWaveRoute(path, route)));\n const pages = estate.pages.filter((page) => (LOC_INDEX_FAMILIES as readonly string[]).includes(page.family ?? \"\"));\n for (const missing of estate.missingHtml.filter((item) => item.location)) {\n failures.push(fail(\"LOC-INDEX\", missing.path, `${missing.path} is a prerendered location route with no HTML in the build, so it cannot be served or indexed.`));\n }\n if (pages.length === 0 && waves.length === 0) return [...failures, notApplicable(\"LOC-INDEX\", \"The build has no location pages.\")];\n if (estate.sitemap === null && pages.length > 0) {\n failures.push(fail(\"LOC-INDEX\", undefined, \"The build holds no sitemap file, so whether every location page is listed could not be checked.\"));\n }\n\n const plan = strategy.estate;\n const indexedNow = (path: string) => plan?.indexedNow.some((route) => matchesWaveRoute(path, route)) ?? false;\n let live = 0;\n let scheduled = 0;\n for (const page of pages) {\n const wave = waveOf(page.path);\n if (plan && !wave && !indexedNow(page.path) && page.family !== \"locations-hub\" && page.family !== \"council\") {\n failures.push(fail(\"LOC-INDEX\", page.path, `${page.path} is a location page the estate plan does not account for; list it in estate.indexedNow or in a wave.`));\n }\n const now = indexedNow(page.path);\n const waiting = wave !== undefined && wave.releaseOn > today;\n if (waiting && !now) {\n scheduled += 1;\n continue;\n }\n // A page indexed now is judged now, even when a wave's pattern also covers it; a wave naming a page the plan's\n // pattern indexes now is a contradiction (the same path in both lists is reported with the plan's problems).\n if (waiting && wave.routes.includes(page.path) && !plan?.indexedNow.includes(page.path)) {\n failures.push(fail(\"LOC-INDEX\", page.path, `${page.path} is indexed now in the estate plan, but the wave of ${wave.releaseOn} still lists it; a page indexed now never waits for a wave.`));\n }\n const dated = waiting ? \"it is indexed now in the estate plan\" : wave ? `its wave was due on ${wave.releaseOn}` : \"no upcoming wave in the recorded schedule dates it\";\n const problems: string[] = [];\n if (!page.indexable) problems.push(\"is marked noindex\");\n if (estate.sitemap && !estate.sitemap.has(page.path)) problems.push(\"is not in the sitemap\");\n if (problems.length > 0) {\n failures.push(fail(\"LOC-INDEX\", page.path, `${page.path} is a location page that ${problems.join(\" and \")}, and ${dated}; every location page is indexable and listed.`));\n } else live += 1;\n }\n\n for (const wave of waves) {\n if (wave.releaseOn > today) continue;\n for (const route of wave.routes) {\n const matched = route.includes(\"*\") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);\n if (!matched) {\n failures.push(fail(\"LOC-INDEX\", route, `The wave of ${wave.releaseOn} released ${route}, but the build has no ${route.includes(\"*\") ? \"page matching it\" : \"such page\"}.`));\n }\n }\n }\n // A return wave brings back pages that are live now, so its pages are in the build before the day.\n for (const wave of waves) {\n if (wave.kind !== \"return\" || wave.releaseOn <= today) continue;\n for (const route of wave.routes) {\n const matched = route.includes(\"*\") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);\n if (!matched) failures.push(fail(\"LOC-INDEX\", route, `The return wave of ${wave.releaseOn} brings back ${route}, but the build has no ${route.includes(\"*\") ? \"page matching it\" : \"such page\"}; a return wave lists pages that are live now.`));\n }\n }\n if (plan) {\n for (const route of plan.indexedNow) {\n const matched = route.includes(\"*\") ? estate.pages.some((page) => matchesWaveRoute(page.path, route)) : estate.byPath.has(route);\n if (!matched) failures.push(fail(\"LOC-INDEX\", route, `estate.indexedNow lists ${route}, but the build has no ${route.includes(\"*\") ? \"page matching it\" : \"such page\"}.`));\n }\n const parent = (template: string | undefined) => template?.slice(0, template.lastIndexOf(\"/\"));\n const shared = plan.paths?.region !== undefined && parent(plan.paths.region) === parent(plan.paths.suburb);\n for (const page of shared ? pages : []) {\n if (page.place?.kind !== \"region\") continue;\n const slug = page.path.slice(page.path.lastIndexOf(\"/\") + 1);\n if (!context.geography?.suburbs.has(slug) || page.path !== `${parent(plan.paths?.region)}/${slug}`) continue;\n if (plan.slugs?.suburbs?.[slug] !== `${slug}-suburb`) {\n failures.push(fail(\"LOC-INDEX\", page.path, `The region page ${page.path} takes the slug of the suburb ${slug}; that suburb's pages take \"${slug}-suburb\" (estate.slugs.suburbs).`));\n }\n }\n }\n const upcoming = scheduled > 0 ? `; ${plural(scheduled, \"more page\")} ${scheduled === 1 ? \"is\" : \"are\"} dated by upcoming waves` : \"\";\n return outcome(\"LOC-INDEX\", failures, `All ${plural(live, \"released location page\")} are indexable and in the sitemap${upcoming}.`);\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export declare const ROUTE_CATALOGUE_FILE = "src/data/routes.ts";
2
+ /** Catalogue hub kinds and the page family their pages are (site-core's section-order hub families). */
3
+ export declare const CATALOGUE_HUB_FAMILIES: Readonly<Record<string, string>>;
4
+ /** Path → kind for every literal catalogue entry; empty when the file is missing or unreadable. */
5
+ export declare function readRouteCatalogueKinds(projectRoot: string): ReadonlyMap<string, string>;
6
+ /**
7
+ * A page's type for L6: its section manifest's family first, then the hub family its catalogue kind gives it, then
8
+ * the family the estate inferred (the estate plan's paths, the sitemap, the route or the stem).
9
+ */
10
+ export declare function linkingPageType(page: {
11
+ readonly path: string;
12
+ readonly family: string | undefined;
13
+ readonly familySource: string | undefined;
14
+ }, catalogueKinds: ReadonlyMap<string, string>): string | undefined;
15
+ //# sourceMappingURL=route-kinds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-kinds.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/route-kinds.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,oBAAoB,uBAAuB,CAAC;AAEzD,wGAAwG;AACxG,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlE,CAAC;AAEH,mGAAmG;AACnG,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,EAC/G,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1C,MAAM,GAAG,SAAS,CAGpB"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The route catalogue's kinds, read from `src/data/routes.ts` without running it (3.15.0). Only entries written as
3
+ * literals are read: hubs are (`{ kind: 'location-index', path: '/locations' }`), while generated entries such as
4
+ * `...suburbs.map(...)` are skipped. The full tier uses them to type hub pages that carry no section manifest, as
5
+ * AA's /locations hub, which links every indexed suburb.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { normalisePath } from "../enquiry/common.js";
10
+ import { LiteralReader, isLiteralObject } from "../enquiry/literal.js";
11
+ export const ROUTE_CATALOGUE_FILE = "src/data/routes.ts";
12
+ /** Catalogue hub kinds and the page family their pages are (site-core's section-order hub families). */
13
+ export const CATALOGUE_HUB_FAMILIES = Object.freeze({
14
+ "location-index": "locations-hub",
15
+ "service-index": "services-hub",
16
+ });
17
+ /** Path → kind for every literal catalogue entry; empty when the file is missing or unreadable. */
18
+ export function readRouteCatalogueKinds(projectRoot) {
19
+ const kinds = new Map();
20
+ const file = join(projectRoot, ROUTE_CATALOGUE_FILE);
21
+ if (!existsSync(file))
22
+ return kinds;
23
+ let reader;
24
+ try {
25
+ reader = new LiteralReader(readFileSync(file, "utf8"), ROUTE_CATALOGUE_FILE);
26
+ }
27
+ catch {
28
+ return kinds;
29
+ }
30
+ const argument = reader.callArguments("defineRouteCatalog")[0] ?? reader.constant("routes");
31
+ const value = argument ? reader.evaluate(argument) : undefined;
32
+ if (!Array.isArray(value))
33
+ return kinds;
34
+ for (const entry of value) {
35
+ if (!isLiteralObject(entry))
36
+ continue;
37
+ const { kind, path } = entry;
38
+ if (typeof kind === "string" && typeof path === "string" && path.startsWith("/"))
39
+ kinds.set(normalisePath(path), kind);
40
+ }
41
+ return kinds;
42
+ }
43
+ /**
44
+ * A page's type for L6: its section manifest's family first, then the hub family its catalogue kind gives it, then
45
+ * the family the estate inferred (the estate plan's paths, the sitemap, the route or the stem).
46
+ */
47
+ export function linkingPageType(page, catalogueKinds) {
48
+ if (page.familySource === "manifest")
49
+ return page.family;
50
+ return CATALOGUE_HUB_FAMILIES[catalogueKinds.get(page.path) ?? ""] ?? page.family;
51
+ }
52
+ //# sourceMappingURL=route-kinds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-kinds.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/route-kinds.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAgB,MAAM,uBAAuB,CAAC;AAErF,MAAM,CAAC,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AAEzD,wGAAwG;AACxG,MAAM,CAAC,MAAM,sBAAsB,GAAqC,MAAM,CAAC,MAAM,CAAC;IACpF,gBAAgB,EAAE,eAAe;IACjC,eAAe,EAAE,cAAc;CAChC,CAAC,CAAC;AAEH,mGAAmG;AACnG,MAAM,UAAU,uBAAuB,CAAC,WAAmB;IACzD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,oBAAoB,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC5F,MAAM,KAAK,GAAwB,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;YAAE,SAAS;QACtC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;QAC7B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACzH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA+G,EAC/G,cAA2C;IAE3C,IAAI,IAAI,CAAC,YAAY,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IACzD,OAAO,sBAAsB,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC;AACpF,CAAC","sourcesContent":["/**\n * The route catalogue's kinds, read from `src/data/routes.ts` without running it (3.15.0). Only entries written as\n * literals are read: hubs are (`{ kind: 'location-index', path: '/locations' }`), while generated entries such as\n * `...suburbs.map(...)` are skipped. The full tier uses them to type hub pages that carry no section manifest, as\n * AA's /locations hub, which links every indexed suburb.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { normalisePath } from \"../enquiry/common.js\";\nimport { LiteralReader, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\n\nexport const ROUTE_CATALOGUE_FILE = \"src/data/routes.ts\";\n\n/** Catalogue hub kinds and the page family their pages are (site-core's section-order hub families). */\nexport const CATALOGUE_HUB_FAMILIES: Readonly<Record<string, string>> = Object.freeze({\n \"location-index\": \"locations-hub\",\n \"service-index\": \"services-hub\",\n});\n\n/** Path → kind for every literal catalogue entry; empty when the file is missing or unreadable. */\nexport function readRouteCatalogueKinds(projectRoot: string): ReadonlyMap<string, string> {\n const kinds = new Map<string, string>();\n const file = join(projectRoot, ROUTE_CATALOGUE_FILE);\n if (!existsSync(file)) return kinds;\n let reader: LiteralReader;\n try {\n reader = new LiteralReader(readFileSync(file, \"utf8\"), ROUTE_CATALOGUE_FILE);\n } catch {\n return kinds;\n }\n const argument = reader.callArguments(\"defineRouteCatalog\")[0] ?? reader.constant(\"routes\");\n const value: Literal | undefined = argument ? reader.evaluate(argument) : undefined;\n if (!Array.isArray(value)) return kinds;\n for (const entry of value) {\n if (!isLiteralObject(entry)) continue;\n const { kind, path } = entry;\n if (typeof kind === \"string\" && typeof path === \"string\" && path.startsWith(\"/\")) kinds.set(normalisePath(path), kind);\n }\n return kinds;\n}\n\n/**\n * A page's type for L6: its section manifest's family first, then the hub family its catalogue kind gives it, then\n * the family the estate inferred (the estate plan's paths, the sitemap, the route or the stem).\n */\nexport function linkingPageType(\n page: { readonly path: string; readonly family: string | undefined; readonly familySource: string | undefined },\n catalogueKinds: ReadonlyMap<string, string>,\n): string | undefined {\n if (page.familySource === \"manifest\") return page.family;\n return CATALOGUE_HUB_FAMILIES[catalogueKinds.get(page.path) ?? \"\"] ?? page.family;\n}\n"]}
@@ -1,4 +1,7 @@
1
+ import { ESTATE_PLAN_SNAPSHOT_FILE } from "../../../estate-snapshot.js";
2
+ import { type SiteEstatePlan } from "@brandlab-digital/site-core/strategy";
1
3
  export declare const FULL_STRATEGY_FILE = "src/data/strategy.ts";
4
+ export { ESTATE_PLAN_SNAPSHOT_FILE };
2
5
  /** As site-core's SITE_WAVE_MIN_GAP_DAYS and SITE_WAVE_MAX_GAP_DAYS. */
3
6
  export declare const WAVE_MIN_GAP_DAYS = 14;
4
7
  export declare const WAVE_MAX_GAP_DAYS = 28;
@@ -13,7 +16,10 @@ export interface FullStrategy {
13
16
  readonly waves: readonly {
14
17
  readonly releaseOn: string;
15
18
  readonly routes: readonly string[];
19
+ readonly kind?: "release" | "return";
16
20
  }[];
21
+ /** An existing estate's plan (site-core 3.14.0 `estate`): pages indexed now, family paths and public slugs; null when none. */
22
+ readonly estate: SiteEstatePlan | null;
17
23
  /** The trade `defineSectionOrder` names, if any. */
18
24
  readonly trade: string | undefined;
19
25
  /** Recorded alternative orders by family. */
@@ -1 +1 @@
1
- {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/strategy.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IACzI,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC9F,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAChE,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,yHAAyH;AACzH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,gHAAgH;AAChH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAOrE;AAKD,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAoEjF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiClJ"}
1
+ {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/strategy.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,yBAAyB,EAAwE,MAAM,6BAA6B,CAAC;AAC9I,OAAO,EAAsB,KAAK,cAAc,EAAwB,MAAM,sCAAsC,CAAC;AAGrH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAGrC,wEAAwE;AACxE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IACzI,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;KAAE,EAAE,CAAC;IACpI,+HAA+H;IAC/H,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAChE,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,yHAAyH;AACzH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,gHAAgH;AAChH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAOrE;AAKD,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAgHjF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiClJ"}
@@ -6,8 +6,12 @@
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { ESTATE_PLAN_SNAPSHOT_FILE, ESTATE_PLAN_SNAPSHOT_KIND, canonicalEstatePlan, estatePlanSourceHash } from "../../../estate-snapshot.js";
10
+ import { estatePlanProblems } from "@brandlab-digital/site-core/strategy";
9
11
  import { LiteralReader, isFullyResolved, isLiteralObject } from "../enquiry/literal.js";
10
12
  export const FULL_STRATEGY_FILE = "src/data/strategy.ts";
13
+ export { ESTATE_PLAN_SNAPSHOT_FILE };
14
+ const SNAPSHOT_REGENERATE = "run the site's tests (writeEstatePlanSnapshot) to write it again, and commit it";
11
15
  /** As site-core's SITE_WAVE_MIN_GAP_DAYS and SITE_WAVE_MAX_GAP_DAYS. */
12
16
  export const WAVE_MIN_GAP_DAYS = 14;
13
17
  export const WAVE_MAX_GAP_DAYS = 28;
@@ -39,7 +43,7 @@ export function readFullStrategy(projectRoot, today) {
39
43
  const empty = { phrases: [], waves: [], orders: [] };
40
44
  const file = join(projectRoot, FULL_STRATEGY_FILE);
41
45
  if (!existsSync(file)) {
42
- return { phrases: null, waves: [], trade: undefined, recordedOrders: new Map(), problems: empty, found: false };
46
+ return { phrases: null, waves: [], estate: null, trade: undefined, recordedOrders: new Map(), problems: empty, found: false };
43
47
  }
44
48
  const reader = new LiteralReader(readFileSync(file, "utf8"), FULL_STRATEGY_FILE);
45
49
  const problems = { phrases: [], waves: [], orders: [] };
@@ -59,26 +63,76 @@ export function readFullStrategy(projectRoot, today) {
59
63
  phrases = { mainTrade: rawPhrases["mainTrade"], owners };
60
64
  }
61
65
  }
62
- const waves = [];
66
+ // The waves and the estate plan: the literal record, or the snapshot a site with a computed plan records.
63
67
  const rawWaves = record?.["waves"];
64
- if (rawWaves !== undefined) {
65
- if (!Array.isArray(rawWaves) || !isFullyResolved(rawWaves)) {
66
- problems.waves.push(`The waves in ${FULL_STRATEGY_FILE} are not a plain list, so the release schedule cannot be read.`);
68
+ const rawEstate = record?.["estate"];
69
+ const wavesReadable = rawWaves === undefined ? Boolean(record) : Array.isArray(rawWaves) && isFullyResolved(rawWaves);
70
+ const estateReadable = rawEstate === undefined ? Boolean(record) : isLiteralObject(rawEstate) && isFullyResolved(rawEstate) && Array.isArray(rawEstate["indexedNow"]);
71
+ const snapshot = readEstateSnapshot(projectRoot);
72
+ let plan;
73
+ if (snapshot === null) {
74
+ plan = { rawWaves, rawEstate, source: FULL_STRATEGY_FILE };
75
+ const hint = `write it as a literal, or record it with writeEstatePlanSnapshot from @brandlab-digital/conformance/estate-snapshot (${ESTATE_PLAN_SNAPSHOT_FILE})`;
76
+ if (rawWaves !== undefined && !wavesReadable)
77
+ problems.waves.push(`The waves in ${FULL_STRATEGY_FILE} are not a plain list, so the release schedule cannot be read; ${hint}.`);
78
+ if (rawEstate !== undefined && !estateReadable) {
79
+ problems.waves.push(`The estate plan in ${FULL_STRATEGY_FILE} is not plain data with an indexedNow list, so the pages indexed now cannot be read; ${hint}.`);
80
+ }
81
+ }
82
+ else if ("problem" in snapshot) {
83
+ plan = { rawWaves: undefined, rawEstate: undefined, source: ESTATE_PLAN_SNAPSHOT_FILE };
84
+ problems.waves.push(snapshot.problem);
85
+ }
86
+ else {
87
+ plan = { rawWaves: snapshot.value["waves"], rawEstate: snapshot.value["estate"], source: ESTATE_PLAN_SNAPSHOT_FILE };
88
+ problems.waves.push(...snapshot.stale);
89
+ if (record && wavesReadable && estateReadable) {
90
+ const literal = canonicalEstatePlan({
91
+ ...(rawWaves !== undefined ? { waves: rawWaves } : {}),
92
+ ...(rawEstate !== undefined ? { estate: rawEstate } : {}),
93
+ });
94
+ const recorded = { ...(snapshot.value["waves"] !== undefined ? { waves: snapshot.value["waves"] } : {}), ...(snapshot.value["estate"] !== undefined ? { estate: snapshot.value["estate"] } : {}) };
95
+ if (JSON.stringify(literal) !== JSON.stringify(recorded)) {
96
+ problems.waves.push(`${ESTATE_PLAN_SNAPSHOT_FILE} does not match the waves and estate plan in ${FULL_STRATEGY_FILE}, so it is stale; ${SNAPSHOT_REGENERATE}.`);
97
+ }
98
+ }
99
+ }
100
+ const waves = [];
101
+ if (plan.rawWaves !== undefined) {
102
+ if (!Array.isArray(plan.rawWaves) || !isFullyResolved(plan.rawWaves)) {
103
+ if (plan.source === ESTATE_PLAN_SNAPSHOT_FILE)
104
+ problems.waves.push(`The waves in ${ESTATE_PLAN_SNAPSHOT_FILE} are not a list; ${SNAPSHOT_REGENERATE}.`);
67
105
  }
68
106
  else {
69
- for (const [index, wave] of rawWaves.entries()) {
107
+ for (const [index, wave] of plan.rawWaves.entries()) {
70
108
  if (!isLiteralObject(wave) || typeof wave["releaseOn"] !== "string" || !Array.isArray(wave["routes"]) || !wave["routes"].every((route) => typeof route === "string")) {
71
- problems.waves.push(`Wave ${index + 1} in ${FULL_STRATEGY_FILE} needs a releaseOn date and a list of routes.`);
109
+ problems.waves.push(`Wave ${index + 1} in ${plan.source} needs a releaseOn date and a list of routes.`);
72
110
  continue;
73
111
  }
74
- waves.push({ releaseOn: wave["releaseOn"], routes: wave["routes"] });
112
+ const kind = wave["kind"];
113
+ if (kind !== undefined && kind !== "release" && kind !== "return") {
114
+ problems.waves.push(`Wave ${index + 1} in ${plan.source} has the kind "${String(kind)}"; a wave is a release or a return.`);
115
+ }
116
+ waves.push({ releaseOn: wave["releaseOn"], routes: wave["routes"], ...(kind === "return" || kind === "release" ? { kind } : {}) });
75
117
  }
76
118
  problems.waves.push(...waveScheduleProblems(waves, today));
77
119
  }
78
120
  }
121
+ let estate = null;
122
+ if (plan.rawEstate !== undefined) {
123
+ if (!isLiteralObject(plan.rawEstate) || !isFullyResolved(plan.rawEstate) || !Array.isArray(plan.rawEstate["indexedNow"])) {
124
+ if (plan.source === ESTATE_PLAN_SNAPSHOT_FILE)
125
+ problems.waves.push(`The estate plan in ${ESTATE_PLAN_SNAPSHOT_FILE} has no indexedNow list; ${SNAPSHOT_REGENERATE}.`);
126
+ }
127
+ else {
128
+ estate = plan.rawEstate;
129
+ problems.waves.push(...estatePlanProblems(estate, waves, today).map((problem) => `${plan.source}: ${problem}`));
130
+ }
131
+ }
79
132
  if (argument && !record) {
80
133
  problems.phrases.push(`${FULL_STRATEGY_FILE} is not plain data, so phrase ownership cannot be read.`);
81
- problems.waves.push(`${FULL_STRATEGY_FILE} is not plain data, so the release schedule cannot be read.`);
134
+ if (snapshot === null)
135
+ problems.waves.push(`${FULL_STRATEGY_FILE} is not plain data, so the release schedule cannot be read.`);
82
136
  }
83
137
  let trade;
84
138
  const recordedOrders = new Map();
@@ -101,7 +155,7 @@ export function readFullStrategy(projectRoot, today) {
101
155
  }
102
156
  }
103
157
  }
104
- return { phrases, waves, trade, recordedOrders, problems, found: true };
158
+ return { phrases, waves, estate, trade, recordedOrders, problems, found: true };
105
159
  }
106
160
  /**
107
161
  * The schedule's rules, as site-core's: valid dates, in order, 14 to 28 days apart, each route in one wave, and the next
@@ -144,4 +198,38 @@ export function waveScheduleProblems(waves, today) {
144
198
  }
145
199
  return problems;
146
200
  }
201
+ /**
202
+ * The estate plan snapshot (`writeEstatePlanSnapshot` in ./estate-snapshot, 3.15.0), or null when the site has none. `stale` lists
203
+ * why it no longer follows its sources: the files it hashed have changed since it was written.
204
+ */
205
+ function readEstateSnapshot(projectRoot) {
206
+ const file = join(projectRoot, ESTATE_PLAN_SNAPSHOT_FILE);
207
+ if (!existsSync(file))
208
+ return null;
209
+ let value;
210
+ try {
211
+ value = JSON.parse(readFileSync(file, "utf8"));
212
+ }
213
+ catch {
214
+ return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE} is not valid JSON; ${SNAPSHOT_REGENERATE}.` };
215
+ }
216
+ const record = value;
217
+ const sources = isLiteralObject(record) ? record["sources"] : undefined;
218
+ if (!isLiteralObject(record) || record["kind"] !== ESTATE_PLAN_SNAPSHOT_KIND || !isLiteralObject(sources) || typeof sources["sha256"] !== "string"
219
+ || !Array.isArray(sources["paths"]) || !sources["paths"].every((path) => typeof path === "string")) {
220
+ return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE} is not an estate plan snapshot (no kind "${ESTATE_PLAN_SNAPSHOT_KIND}" with its sources); ${SNAPSHOT_REGENERATE}.` };
221
+ }
222
+ const paths = sources["paths"];
223
+ let current;
224
+ try {
225
+ current = estatePlanSourceHash(projectRoot, paths);
226
+ }
227
+ catch (error) {
228
+ return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE}: ${error instanceof Error ? error.message : String(error)}` };
229
+ }
230
+ const stale = current === sources["sha256"]
231
+ ? []
232
+ : [`${ESTATE_PLAN_SNAPSHOT_FILE} is older than the files it was written from (${paths.join(", ")} changed since), so it may not match the plan; ${SNAPSHOT_REGENERATE}.`];
233
+ return { value: record, stale };
234
+ }
147
235
  //# sourceMappingURL=strategy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"strategy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/strategy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAgB,MAAM,uBAAuB,CAAC;AAEtG,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD,wEAAwE;AACxE,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAcpC,yHAAyH;AACzH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,MAAM;SACV,WAAW,EAAE;SACb,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,IAAI,EAAE;SACN,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SACzG,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,KAAK,KAAK,CAAC;IAChD,MAAM,MAAM,GAAG,KAAK;SACjB,KAAK,CAAC,WAAW,CAAC;SAClB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,CAAC;SAC3G,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,OAAO,IAAI,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,OAAO,GAAG,qBAAqB,CAAC;AACtC,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,KAAa;IACjE,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,EAAc,EAAE,KAAK,EAAE,EAAc,EAAE,MAAM,EAAE,EAAc,EAAE,CAAC;IACzF,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC;IACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,GAAG,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAClH,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACjF,MAAM,QAAQ,GAAG,EAAE,OAAO,EAAE,EAAc,EAAE,KAAK,EAAE,EAAc,EAAE,MAAM,EAAE,EAAc,EAAE,CAAC;IAE5F,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9F,MAAM,KAAK,GAAwB,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpF,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1D,IAAI,OAAO,GAA4B,IAAI,CAAC;IAC5C,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,IAAI,OAAO,UAAU,CAAC,WAAW,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACxJ,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,kBAAkB,8FAA8F,CAAC,CAAC;QAC5J,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CACpD,eAAe,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnK,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM;gBAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,kBAAkB,0BAA0B,CAAC,CAAC;YACtI,OAAO,GAAG,EAAE,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAA8C,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3D,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,kBAAkB,gEAAgE,CAAC,CAAC;QAC1H,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC/C,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,EAAE,CAAC;oBACrK,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,KAAK,GAAG,CAAC,OAAO,kBAAkB,+CAA+C,CAAC,CAAC;oBAC/G,SAAS;gBACX,CAAC;gBACD,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAa,EAAE,CAAC,CAAC;YACnF,CAAC;YACD,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,kBAAkB,yDAAyD,CAAC,CAAC;QACtG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,6DAA6D,CAAC,CAAC;IAC1G,CAAC;IAED,IAAI,KAAyB,CAAC;IAC9B,MAAM,cAAc,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnD,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;QAC9C,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,kBAAkB,gEAAgE,CAAC,CAAC;QACpI,CAAC;aAAM,CAAC;YACN,IAAI,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ;gBAAE,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;YACpC,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC9B,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACvD,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,EAAE,CAAC;wBACpH,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAa,CAAC,CAAC;oBACzD,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC1E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAoF,EAAE,KAAa;IACtI,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,QAAiD,CAAC;IACtD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;QAClG,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACrF,QAAQ,CAAC,IAAI,CAAC,QAAQ,KAAK,GAAG,CAAC,OAAO,kBAAkB,kCAAkC,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC;YAC/G,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,OAAO,kBAAkB,mBAAmB,CAAC,CAAC;QACvH,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,WAAW,KAAK,uCAAuC,CAAC,CAAC;YAChI,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,iCAAiC,kBAAkB,GAAG,CAAC,CAAC;YACpG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;QACD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;YAC1C,IAAI,GAAG,GAAG,iBAAiB,IAAI,GAAG,GAAG,iBAAiB,EAAE,CAAC;gBACvD,QAAQ,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,OAAO,GAAG,2BAA2B,QAAQ,CAAC,EAAE,iCAAiC,iBAAiB,OAAO,iBAAiB,cAAc,CAAC,CAAC;YACvL,CAAC;QACH,CAAC;QACD,QAAQ,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC;IAC/D,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;QAC3D,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,CAAE,CAAC,SAAS,YAAY,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC;QAC3G,IAAI,GAAG,GAAG,iBAAiB,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,qBAAqB,KAAK,CAAC,IAAI,CAAE,CAAC,SAAS,QAAQ,GAAG,eAAe,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,2BAA2B,IAAI,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,GAAG,wBAAwB,iBAAiB,qCAAqC,CAAC,CAAC;QACtO,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["/**\n * What the full tier reads from `src/data/strategy.ts` without running it: phrase ownership and the wave schedule\n * (`defineSiteStrategy`, site-core 3.12.0) and the section orders (`defineSectionOrder`: the trade and any recorded\n * alternative order per family). The wave rules mirror site-core's, because conformance compiles without site-core;\n * strategy.test.ts keeps the two equal.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { LiteralReader, isFullyResolved, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\n\nexport const FULL_STRATEGY_FILE = \"src/data/strategy.ts\";\n\n/** As site-core's SITE_WAVE_MIN_GAP_DAYS and SITE_WAVE_MAX_GAP_DAYS. */\nexport const WAVE_MIN_GAP_DAYS = 14;\nexport const WAVE_MAX_GAP_DAYS = 28;\n\nexport interface FullStrategy {\n readonly phrases: { readonly mainTrade: string; readonly owners: readonly { readonly phrase: string; readonly route: string }[] } | null;\n readonly waves: readonly { readonly releaseOn: string; readonly routes: readonly string[] }[];\n /** The trade `defineSectionOrder` names, if any. */\n readonly trade: string | undefined;\n /** Recorded alternative orders by family. */\n readonly recordedOrders: ReadonlyMap<string, readonly string[]>;\n /** What could not be read or breaks the schedule's rules; the checks that need it fail with these. */\n readonly problems: { readonly phrases: string[]; readonly waves: string[]; readonly orders: string[] };\n readonly found: boolean;\n}\n\n/** As site-core's normaliseSitePhrase: lowercase words, \"&\" read as \"and\", punctuation dropped, a plural \"s\" dropped. */\nexport function normalisePhrase(phrase: string): string {\n return phrase\n .toLowerCase()\n .replace(/&/g, \" and \")\n .replace(/[^a-z0-9]+/g, \" \")\n .trim()\n .split(\" \")\n .map((word) => (word.length > 3 && word.endsWith(\"s\") && !word.endsWith(\"ss\") ? word.slice(0, -1) : word))\n .join(\" \");\n}\n\n/** As site-core's matchesWaveRoute: an exact path, or a pattern with `*` (one segment) and `**` (any depth). */\nexport function matchesWaveRoute(path: string, route: string): boolean {\n if (!route.includes(\"*\")) return path === route;\n const source = route\n .split(/(\\*\\*|\\*)/)\n .map((part) => (part === \"**\" ? \".*\" : part === \"*\" ? \"[^/]*\" : part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")))\n .join(\"\");\n return new RegExp(`^${source}$`).test(path);\n}\n\nconst ISO_DAY = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst DAY_MS = 86_400_000;\n\n/** `today` (Sydney, YYYY-MM-DD) dates the schedule's next wave, which may not wait open-ended. */\nexport function readFullStrategy(projectRoot: string, today: string): FullStrategy {\n const empty = { phrases: [] as string[], waves: [] as string[], orders: [] as string[] };\n const file = join(projectRoot, FULL_STRATEGY_FILE);\n if (!existsSync(file)) {\n return { phrases: null, waves: [], trade: undefined, recordedOrders: new Map(), problems: empty, found: false };\n }\n const reader = new LiteralReader(readFileSync(file, \"utf8\"), FULL_STRATEGY_FILE);\n const problems = { phrases: [] as string[], waves: [] as string[], orders: [] as string[] };\n\n const argument = reader.callArguments(\"defineSiteStrategy\")[0] ?? reader.constant(\"strategy\");\n const value: Literal | undefined = argument ? reader.evaluate(argument) : undefined;\n const record = isLiteralObject(value) ? value : undefined;\n\n let phrases: FullStrategy[\"phrases\"] = null;\n const rawPhrases = record?.[\"phrases\"];\n if (rawPhrases !== undefined) {\n if (!isLiteralObject(rawPhrases) || !isFullyResolved(rawPhrases) || typeof rawPhrases[\"mainTrade\"] !== \"string\" || !Array.isArray(rawPhrases[\"owners\"])) {\n problems.phrases.push(`The phrases in ${FULL_STRATEGY_FILE} are not plain data with a mainTrade and an owners list, so phrase ownership cannot be read.`);\n } else {\n const owners = rawPhrases[\"owners\"].flatMap((owner) =>\n isLiteralObject(owner) && typeof owner[\"phrase\"] === \"string\" && typeof owner[\"route\"] === \"string\" ? [{ phrase: owner[\"phrase\"], route: owner[\"route\"] }] : []);\n if (owners.length !== rawPhrases[\"owners\"].length) problems.phrases.push(`An owner in ${FULL_STRATEGY_FILE} has no phrase or route.`);\n phrases = { mainTrade: rawPhrases[\"mainTrade\"], owners };\n }\n }\n\n const waves: { releaseOn: string; routes: string[] }[] = [];\n const rawWaves = record?.[\"waves\"];\n if (rawWaves !== undefined) {\n if (!Array.isArray(rawWaves) || !isFullyResolved(rawWaves)) {\n problems.waves.push(`The waves in ${FULL_STRATEGY_FILE} are not a plain list, so the release schedule cannot be read.`);\n } else {\n for (const [index, wave] of rawWaves.entries()) {\n if (!isLiteralObject(wave) || typeof wave[\"releaseOn\"] !== \"string\" || !Array.isArray(wave[\"routes\"]) || !wave[\"routes\"].every((route) => typeof route === \"string\")) {\n problems.waves.push(`Wave ${index + 1} in ${FULL_STRATEGY_FILE} needs a releaseOn date and a list of routes.`);\n continue;\n }\n waves.push({ releaseOn: wave[\"releaseOn\"], routes: wave[\"routes\"] as string[] });\n }\n problems.waves.push(...waveScheduleProblems(waves, today));\n }\n }\n if (argument && !record) {\n problems.phrases.push(`${FULL_STRATEGY_FILE} is not plain data, so phrase ownership cannot be read.`);\n problems.waves.push(`${FULL_STRATEGY_FILE} is not plain data, so the release schedule cannot be read.`);\n }\n\n let trade: string | undefined;\n const recordedOrders = new Map<string, string[]>();\n const orderArgument = reader.callArguments(\"defineSectionOrder\")[0];\n if (orderArgument) {\n const orders = reader.evaluate(orderArgument);\n if (!isLiteralObject(orders) || !isFullyResolved(orders)) {\n problems.orders.push(`defineSectionOrder in ${FULL_STRATEGY_FILE} is not plain data, so recorded section orders cannot be read.`);\n } else {\n if (typeof orders[\"trade\"] === \"string\") trade = orders[\"trade\"];\n const recorded = orders[\"recorded\"];\n if (isLiteralObject(recorded)) {\n for (const [family, entry] of Object.entries(recorded)) {\n if (isLiteralObject(entry) && Array.isArray(entry[\"order\"]) && entry[\"order\"].every((id) => typeof id === \"string\")) {\n recordedOrders.set(family, entry[\"order\"] as string[]);\n }\n }\n }\n }\n }\n\n return { phrases, waves, trade, recordedOrders, problems, found: true };\n}\n\n/**\n * The schedule's rules, as site-core's: valid dates, in order, 14 to 28 days apart, each route in one wave, and the next\n * wave still to come at most 28 days after the last released one (or after today when none is).\n */\nexport function waveScheduleProblems(waves: readonly { readonly releaseOn: string; readonly routes: readonly string[] }[], today: string): string[] {\n const problems: string[] = [];\n const seen = new Set<string>();\n let previous: { day: number; on: string } | undefined;\n for (const [index, wave] of waves.entries()) {\n const day = ISO_DAY.test(wave.releaseOn) ? Date.parse(`${wave.releaseOn}T00:00:00Z`) : Number.NaN;\n if (Number.isNaN(day) || new Date(day).toISOString().slice(0, 10) !== wave.releaseOn) {\n problems.push(`Wave ${index + 1} in ${FULL_STRATEGY_FILE} has no valid releaseOn date (\"${wave.releaseOn}\").`);\n continue;\n }\n if (wave.routes.length === 0) problems.push(`The wave of ${wave.releaseOn} in ${FULL_STRATEGY_FILE} lists no routes.`);\n for (const route of wave.routes) {\n if (!route.startsWith(\"/\")) problems.push(`The wave of ${wave.releaseOn} lists \"${route}\", which is not a root-relative path.`);\n if (seen.has(route)) problems.push(`\"${route}\" is in more than one wave in ${FULL_STRATEGY_FILE}.`);\n seen.add(route);\n }\n if (previous) {\n const gap = (day - previous.day) / DAY_MS;\n if (gap < WAVE_MIN_GAP_DAYS || gap > WAVE_MAX_GAP_DAYS) {\n problems.push(`The wave of ${wave.releaseOn} is ${gap} days after the wave of ${previous.on}; waves go out in date order, ${WAVE_MIN_GAP_DAYS} to ${WAVE_MAX_GAP_DAYS} days apart.`);\n }\n }\n previous = { day, on: wave.releaseOn };\n }\n const next = waves.findIndex((wave) => wave.releaseOn > today);\n if (next !== -1 && problems.length === 0) {\n const from = next > 0 ? waves[next - 1]!.releaseOn : today;\n const gap = (Date.parse(`${waves[next]!.releaseOn}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / DAY_MS;\n if (gap > WAVE_MAX_GAP_DAYS) {\n problems.push(`The next wave, on ${waves[next]!.releaseOn}, is ${gap} days after ${next > 0 ? `the last released wave (${from})` : `today (${from})`}; it goes out within ${WAVE_MAX_GAP_DAYS} days, so no page waits open-ended.`);\n }\n }\n return problems;\n}\n"]}
1
+ {"version":3,"file":"strategy.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the full tier reads from `src/data/strategy.ts` without running it: phrase ownership and the wave schedule\n * (`defineSiteStrategy`, site-core 3.12.0) and the section orders (`defineSectionOrder`: the trade and any recorded\n * alternative order per family). The wave rules mirror site-core's, because conformance compiles without site-core;\n * strategy.test.ts keeps the two equal.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { ESTATE_PLAN_SNAPSHOT_FILE, ESTATE_PLAN_SNAPSHOT_KIND, canonicalEstatePlan, estatePlanSourceHash } from \"../../../estate-snapshot.js\";\nimport { estatePlanProblems, type SiteEstatePlan, type SiteReleaseWave } from \"@brandlab-digital/site-core/strategy\";\nimport { LiteralReader, isFullyResolved, isLiteralObject, type Literal } from \"../enquiry/literal.js\";\n\nexport const FULL_STRATEGY_FILE = \"src/data/strategy.ts\";\nexport { ESTATE_PLAN_SNAPSHOT_FILE };\nconst SNAPSHOT_REGENERATE = \"run the site's tests (writeEstatePlanSnapshot) to write it again, and commit it\";\n\n/** As site-core's SITE_WAVE_MIN_GAP_DAYS and SITE_WAVE_MAX_GAP_DAYS. */\nexport const WAVE_MIN_GAP_DAYS = 14;\nexport const WAVE_MAX_GAP_DAYS = 28;\n\nexport interface FullStrategy {\n readonly phrases: { readonly mainTrade: string; readonly owners: readonly { readonly phrase: string; readonly route: string }[] } | null;\n readonly waves: readonly { readonly releaseOn: string; readonly routes: readonly string[]; readonly kind?: \"release\" | \"return\" }[];\n /** An existing estate's plan (site-core 3.14.0 `estate`): pages indexed now, family paths and public slugs; null when none. */\n readonly estate: SiteEstatePlan | null;\n /** The trade `defineSectionOrder` names, if any. */\n readonly trade: string | undefined;\n /** Recorded alternative orders by family. */\n readonly recordedOrders: ReadonlyMap<string, readonly string[]>;\n /** What could not be read or breaks the schedule's rules; the checks that need it fail with these. */\n readonly problems: { readonly phrases: string[]; readonly waves: string[]; readonly orders: string[] };\n readonly found: boolean;\n}\n\n/** As site-core's normaliseSitePhrase: lowercase words, \"&\" read as \"and\", punctuation dropped, a plural \"s\" dropped. */\nexport function normalisePhrase(phrase: string): string {\n return phrase\n .toLowerCase()\n .replace(/&/g, \" and \")\n .replace(/[^a-z0-9]+/g, \" \")\n .trim()\n .split(\" \")\n .map((word) => (word.length > 3 && word.endsWith(\"s\") && !word.endsWith(\"ss\") ? word.slice(0, -1) : word))\n .join(\" \");\n}\n\n/** As site-core's matchesWaveRoute: an exact path, or a pattern with `*` (one segment) and `**` (any depth). */\nexport function matchesWaveRoute(path: string, route: string): boolean {\n if (!route.includes(\"*\")) return path === route;\n const source = route\n .split(/(\\*\\*|\\*)/)\n .map((part) => (part === \"**\" ? \".*\" : part === \"*\" ? \"[^/]*\" : part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\")))\n .join(\"\");\n return new RegExp(`^${source}$`).test(path);\n}\n\nconst ISO_DAY = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst DAY_MS = 86_400_000;\n\n/** `today` (Sydney, YYYY-MM-DD) dates the schedule's next wave, which may not wait open-ended. */\nexport function readFullStrategy(projectRoot: string, today: string): FullStrategy {\n const empty = { phrases: [] as string[], waves: [] as string[], orders: [] as string[] };\n const file = join(projectRoot, FULL_STRATEGY_FILE);\n if (!existsSync(file)) {\n return { phrases: null, waves: [], estate: null, trade: undefined, recordedOrders: new Map(), problems: empty, found: false };\n }\n const reader = new LiteralReader(readFileSync(file, \"utf8\"), FULL_STRATEGY_FILE);\n const problems = { phrases: [] as string[], waves: [] as string[], orders: [] as string[] };\n\n const argument = reader.callArguments(\"defineSiteStrategy\")[0] ?? reader.constant(\"strategy\");\n const value: Literal | undefined = argument ? reader.evaluate(argument) : undefined;\n const record = isLiteralObject(value) ? value : undefined;\n\n let phrases: FullStrategy[\"phrases\"] = null;\n const rawPhrases = record?.[\"phrases\"];\n if (rawPhrases !== undefined) {\n if (!isLiteralObject(rawPhrases) || !isFullyResolved(rawPhrases) || typeof rawPhrases[\"mainTrade\"] !== \"string\" || !Array.isArray(rawPhrases[\"owners\"])) {\n problems.phrases.push(`The phrases in ${FULL_STRATEGY_FILE} are not plain data with a mainTrade and an owners list, so phrase ownership cannot be read.`);\n } else {\n const owners = rawPhrases[\"owners\"].flatMap((owner) =>\n isLiteralObject(owner) && typeof owner[\"phrase\"] === \"string\" && typeof owner[\"route\"] === \"string\" ? [{ phrase: owner[\"phrase\"], route: owner[\"route\"] }] : []);\n if (owners.length !== rawPhrases[\"owners\"].length) problems.phrases.push(`An owner in ${FULL_STRATEGY_FILE} has no phrase or route.`);\n phrases = { mainTrade: rawPhrases[\"mainTrade\"], owners };\n }\n }\n\n // The waves and the estate plan: the literal record, or the snapshot a site with a computed plan records.\n const rawWaves = record?.[\"waves\"];\n const rawEstate = record?.[\"estate\"];\n const wavesReadable = rawWaves === undefined ? Boolean(record) : Array.isArray(rawWaves) && isFullyResolved(rawWaves);\n const estateReadable = rawEstate === undefined ? Boolean(record) : isLiteralObject(rawEstate) && isFullyResolved(rawEstate) && Array.isArray(rawEstate[\"indexedNow\"]);\n const snapshot = readEstateSnapshot(projectRoot);\n let plan: { rawWaves: Literal | undefined; rawEstate: Literal | undefined; source: string };\n if (snapshot === null) {\n plan = { rawWaves, rawEstate, source: FULL_STRATEGY_FILE };\n const hint = `write it as a literal, or record it with writeEstatePlanSnapshot from @brandlab-digital/conformance/estate-snapshot (${ESTATE_PLAN_SNAPSHOT_FILE})`;\n if (rawWaves !== undefined && !wavesReadable) problems.waves.push(`The waves in ${FULL_STRATEGY_FILE} are not a plain list, so the release schedule cannot be read; ${hint}.`);\n if (rawEstate !== undefined && !estateReadable) {\n problems.waves.push(`The estate plan in ${FULL_STRATEGY_FILE} is not plain data with an indexedNow list, so the pages indexed now cannot be read; ${hint}.`);\n }\n } else if (\"problem\" in snapshot) {\n plan = { rawWaves: undefined, rawEstate: undefined, source: ESTATE_PLAN_SNAPSHOT_FILE };\n problems.waves.push(snapshot.problem);\n } else {\n plan = { rawWaves: snapshot.value[\"waves\"], rawEstate: snapshot.value[\"estate\"], source: ESTATE_PLAN_SNAPSHOT_FILE };\n problems.waves.push(...snapshot.stale);\n if (record && wavesReadable && estateReadable) {\n const literal = canonicalEstatePlan({\n ...(rawWaves !== undefined ? { waves: rawWaves as unknown as SiteReleaseWave[] } : {}),\n ...(rawEstate !== undefined ? { estate: rawEstate as unknown as SiteEstatePlan } : {}),\n });\n const recorded = { ...(snapshot.value[\"waves\"] !== undefined ? { waves: snapshot.value[\"waves\"] } : {}), ...(snapshot.value[\"estate\"] !== undefined ? { estate: snapshot.value[\"estate\"] } : {}) };\n if (JSON.stringify(literal) !== JSON.stringify(recorded)) {\n problems.waves.push(`${ESTATE_PLAN_SNAPSHOT_FILE} does not match the waves and estate plan in ${FULL_STRATEGY_FILE}, so it is stale; ${SNAPSHOT_REGENERATE}.`);\n }\n }\n }\n\n const waves: { releaseOn: string; routes: string[]; kind?: \"release\" | \"return\" }[] = [];\n if (plan.rawWaves !== undefined) {\n if (!Array.isArray(plan.rawWaves) || !isFullyResolved(plan.rawWaves)) {\n if (plan.source === ESTATE_PLAN_SNAPSHOT_FILE) problems.waves.push(`The waves in ${ESTATE_PLAN_SNAPSHOT_FILE} are not a list; ${SNAPSHOT_REGENERATE}.`);\n } else {\n for (const [index, wave] of plan.rawWaves.entries()) {\n if (!isLiteralObject(wave) || typeof wave[\"releaseOn\"] !== \"string\" || !Array.isArray(wave[\"routes\"]) || !wave[\"routes\"].every((route) => typeof route === \"string\")) {\n problems.waves.push(`Wave ${index + 1} in ${plan.source} needs a releaseOn date and a list of routes.`);\n continue;\n }\n const kind = wave[\"kind\"];\n if (kind !== undefined && kind !== \"release\" && kind !== \"return\") {\n problems.waves.push(`Wave ${index + 1} in ${plan.source} has the kind \"${String(kind)}\"; a wave is a release or a return.`);\n }\n waves.push({ releaseOn: wave[\"releaseOn\"], routes: wave[\"routes\"] as string[], ...(kind === \"return\" || kind === \"release\" ? { kind } : {}) });\n }\n problems.waves.push(...waveScheduleProblems(waves, today));\n }\n }\n let estate: SiteEstatePlan | null = null;\n if (plan.rawEstate !== undefined) {\n if (!isLiteralObject(plan.rawEstate) || !isFullyResolved(plan.rawEstate) || !Array.isArray(plan.rawEstate[\"indexedNow\"])) {\n if (plan.source === ESTATE_PLAN_SNAPSHOT_FILE) problems.waves.push(`The estate plan in ${ESTATE_PLAN_SNAPSHOT_FILE} has no indexedNow list; ${SNAPSHOT_REGENERATE}.`);\n } else {\n estate = plan.rawEstate as unknown as SiteEstatePlan;\n problems.waves.push(...estatePlanProblems(estate, waves, today).map((problem) => `${plan.source}: ${problem}`));\n }\n }\n if (argument && !record) {\n problems.phrases.push(`${FULL_STRATEGY_FILE} is not plain data, so phrase ownership cannot be read.`);\n if (snapshot === null) problems.waves.push(`${FULL_STRATEGY_FILE} is not plain data, so the release schedule cannot be read.`);\n }\n\n let trade: string | undefined;\n const recordedOrders = new Map<string, string[]>();\n const orderArgument = reader.callArguments(\"defineSectionOrder\")[0];\n if (orderArgument) {\n const orders = reader.evaluate(orderArgument);\n if (!isLiteralObject(orders) || !isFullyResolved(orders)) {\n problems.orders.push(`defineSectionOrder in ${FULL_STRATEGY_FILE} is not plain data, so recorded section orders cannot be read.`);\n } else {\n if (typeof orders[\"trade\"] === \"string\") trade = orders[\"trade\"];\n const recorded = orders[\"recorded\"];\n if (isLiteralObject(recorded)) {\n for (const [family, entry] of Object.entries(recorded)) {\n if (isLiteralObject(entry) && Array.isArray(entry[\"order\"]) && entry[\"order\"].every((id) => typeof id === \"string\")) {\n recordedOrders.set(family, entry[\"order\"] as string[]);\n }\n }\n }\n }\n }\n\n return { phrases, waves, estate, trade, recordedOrders, problems, found: true };\n}\n\n/**\n * The schedule's rules, as site-core's: valid dates, in order, 14 to 28 days apart, each route in one wave, and the next\n * wave still to come at most 28 days after the last released one (or after today when none is).\n */\nexport function waveScheduleProblems(waves: readonly { readonly releaseOn: string; readonly routes: readonly string[] }[], today: string): string[] {\n const problems: string[] = [];\n const seen = new Set<string>();\n let previous: { day: number; on: string } | undefined;\n for (const [index, wave] of waves.entries()) {\n const day = ISO_DAY.test(wave.releaseOn) ? Date.parse(`${wave.releaseOn}T00:00:00Z`) : Number.NaN;\n if (Number.isNaN(day) || new Date(day).toISOString().slice(0, 10) !== wave.releaseOn) {\n problems.push(`Wave ${index + 1} in ${FULL_STRATEGY_FILE} has no valid releaseOn date (\"${wave.releaseOn}\").`);\n continue;\n }\n if (wave.routes.length === 0) problems.push(`The wave of ${wave.releaseOn} in ${FULL_STRATEGY_FILE} lists no routes.`);\n for (const route of wave.routes) {\n if (!route.startsWith(\"/\")) problems.push(`The wave of ${wave.releaseOn} lists \"${route}\", which is not a root-relative path.`);\n if (seen.has(route)) problems.push(`\"${route}\" is in more than one wave in ${FULL_STRATEGY_FILE}.`);\n seen.add(route);\n }\n if (previous) {\n const gap = (day - previous.day) / DAY_MS;\n if (gap < WAVE_MIN_GAP_DAYS || gap > WAVE_MAX_GAP_DAYS) {\n problems.push(`The wave of ${wave.releaseOn} is ${gap} days after the wave of ${previous.on}; waves go out in date order, ${WAVE_MIN_GAP_DAYS} to ${WAVE_MAX_GAP_DAYS} days apart.`);\n }\n }\n previous = { day, on: wave.releaseOn };\n }\n const next = waves.findIndex((wave) => wave.releaseOn > today);\n if (next !== -1 && problems.length === 0) {\n const from = next > 0 ? waves[next - 1]!.releaseOn : today;\n const gap = (Date.parse(`${waves[next]!.releaseOn}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / DAY_MS;\n if (gap > WAVE_MAX_GAP_DAYS) {\n problems.push(`The next wave, on ${waves[next]!.releaseOn}, is ${gap} days after ${next > 0 ? `the last released wave (${from})` : `today (${from})`}; it goes out within ${WAVE_MAX_GAP_DAYS} days, so no page waits open-ended.`);\n }\n }\n return problems;\n}\n\n/**\n * The estate plan snapshot (`writeEstatePlanSnapshot` in ./estate-snapshot, 3.15.0), or null when the site has none. `stale` lists\n * why it no longer follows its sources: the files it hashed have changed since it was written.\n */\nfunction readEstateSnapshot(projectRoot: string): { value: { [key: string]: Literal }; stale: string[] } | { problem: string } | null {\n const file = join(projectRoot, ESTATE_PLAN_SNAPSHOT_FILE);\n if (!existsSync(file)) return null;\n let value: unknown;\n try {\n value = JSON.parse(readFileSync(file, \"utf8\"));\n } catch {\n return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE} is not valid JSON; ${SNAPSHOT_REGENERATE}.` };\n }\n const record = value as { [key: string]: Literal };\n const sources = isLiteralObject(record) ? record[\"sources\"] : undefined;\n if (!isLiteralObject(record) || record[\"kind\"] !== ESTATE_PLAN_SNAPSHOT_KIND || !isLiteralObject(sources) || typeof sources[\"sha256\"] !== \"string\"\n || !Array.isArray(sources[\"paths\"]) || !sources[\"paths\"].every((path) => typeof path === \"string\")) {\n return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE} is not an estate plan snapshot (no kind \"${ESTATE_PLAN_SNAPSHOT_KIND}\" with its sources); ${SNAPSHOT_REGENERATE}.` };\n }\n const paths = sources[\"paths\"] as string[];\n let current: string;\n try {\n current = estatePlanSourceHash(projectRoot, paths);\n } catch (error) {\n return { problem: `${ESTATE_PLAN_SNAPSHOT_FILE}: ${error instanceof Error ? error.message : String(error)}` };\n }\n const stale = current === sources[\"sha256\"]\n ? []\n : [`${ESTATE_PLAN_SNAPSHOT_FILE} is older than the files it was written from (${paths.join(\", \")} changed since), so it may not match the plan; ${SNAPSHOT_REGENERATE}.`];\n return { value: record, stale };\n}\n"]}