@brandlab-digital/conformance 3.12.1 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +41 -1
  2. package/dist/analytics-e2e.d.ts +10 -16
  3. package/dist/analytics-e2e.d.ts.map +1 -1
  4. package/dist/analytics-e2e.js +61 -3
  5. package/dist/analytics-e2e.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/current-release.js +5 -5
  10. package/dist/current-release.js.map +1 -1
  11. package/dist/edge-probe-cli.d.ts +57 -0
  12. package/dist/edge-probe-cli.d.ts.map +1 -0
  13. package/dist/edge-probe-cli.js +134 -0
  14. package/dist/edge-probe-cli.js.map +1 -0
  15. package/dist/engineering.d.ts +4 -1
  16. package/dist/engineering.d.ts.map +1 -1
  17. package/dist/engineering.js +19 -14
  18. package/dist/engineering.js.map +1 -1
  19. package/dist/estate-sample.d.ts +60 -0
  20. package/dist/estate-sample.d.ts.map +1 -0
  21. package/dist/estate-sample.js +144 -0
  22. package/dist/estate-sample.js.map +1 -0
  23. package/dist/estate.d.ts +45 -0
  24. package/dist/estate.d.ts.map +1 -1
  25. package/dist/estate.js +82 -22
  26. package/dist/estate.js.map +1 -1
  27. package/dist/origin-region.d.ts +14 -0
  28. package/dist/origin-region.d.ts.map +1 -0
  29. package/dist/origin-region.js +46 -0
  30. package/dist/origin-region.js.map +1 -0
  31. package/dist/prefetch-probe-cli.d.ts +36 -0
  32. package/dist/prefetch-probe-cli.d.ts.map +1 -0
  33. package/dist/prefetch-probe-cli.js +163 -0
  34. package/dist/prefetch-probe-cli.js.map +1 -0
  35. package/dist/project.d.ts +20 -0
  36. package/dist/project.d.ts.map +1 -1
  37. package/dist/project.js +92 -2
  38. package/dist/project.js.map +1 -1
  39. package/dist/ready-cli.d.ts.map +1 -1
  40. package/dist/ready-cli.js +8 -3
  41. package/dist/ready-cli.js.map +1 -1
  42. package/dist/rule-registry.d.ts +5 -0
  43. package/dist/rule-registry.d.ts.map +1 -1
  44. package/dist/rule-registry.js +1 -0
  45. package/dist/rule-registry.js.map +1 -1
  46. package/dist/site-system/checks.d.ts +62 -1
  47. package/dist/site-system/checks.d.ts.map +1 -1
  48. package/dist/site-system/checks.js +9 -0
  49. package/dist/site-system/checks.js.map +1 -1
  50. package/dist/site-system/config.d.ts +27 -0
  51. package/dist/site-system/config.d.ts.map +1 -1
  52. package/dist/site-system/config.js +50 -1
  53. package/dist/site-system/config.js.map +1 -1
  54. package/dist/site-system/index.d.ts +1 -1
  55. package/dist/site-system/index.d.ts.map +1 -1
  56. package/dist/site-system/index.js +1 -1
  57. package/dist/site-system/index.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +9 -2
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +11 -1
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
  63. package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
  64. package/dist/site-system/journeys/prefetch-measure.js +94 -0
  65. package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
  66. package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
  67. package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
  68. package/dist/site-system/journeys/prefetch-policy.js +152 -0
  69. package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
  70. package/dist/site-system/journeys/prefetch.d.ts +3 -0
  71. package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
  72. package/dist/site-system/journeys/prefetch.js +37 -0
  73. package/dist/site-system/journeys/prefetch.js.map +1 -0
  74. package/dist/site-system/journeys/variants.d.ts +34 -0
  75. package/dist/site-system/journeys/variants.d.ts.map +1 -0
  76. package/dist/site-system/journeys/variants.js +139 -0
  77. package/dist/site-system/journeys/variants.js.map +1 -0
  78. package/dist/site-system/report.d.ts.map +1 -1
  79. package/dist/site-system/report.js +19 -5
  80. package/dist/site-system/report.js.map +1 -1
  81. package/dist/site-system/rule-modules.d.ts.map +1 -1
  82. package/dist/site-system/rule-modules.js +10 -0
  83. package/dist/site-system/rule-modules.js.map +1 -1
  84. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  85. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  86. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  87. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  88. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  89. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  90. package/dist/site-system/rules/delivery/index.js +7 -0
  91. package/dist/site-system/rules/delivery/index.js.map +1 -0
  92. package/dist/site-system/rules/full/common.d.ts +3 -0
  93. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  94. package/dist/site-system/rules/full/common.js.map +1 -1
  95. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  96. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  97. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  98. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  99. package/dist/site-system/rules/full/estate.d.ts +20 -1
  100. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  101. package/dist/site-system/rules/full/estate.js +33 -5
  102. package/dist/site-system/rules/full/estate.js.map +1 -1
  103. package/dist/site-system/rules/full/geography.d.ts +8 -2
  104. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  105. package/dist/site-system/rules/full/geography.js +6 -2
  106. package/dist/site-system/rules/full/geography.js.map +1 -1
  107. package/dist/site-system/rules/full/index.d.ts +3 -2
  108. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  109. package/dist/site-system/rules/full/index.js +27 -9
  110. package/dist/site-system/rules/full/index.js.map +1 -1
  111. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  112. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  113. package/dist/site-system/rules/full/k12-slots.js +40 -0
  114. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  115. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  116. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  117. package/dist/site-system/rules/full/loc-index.js +43 -2
  118. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  119. package/dist/site-system/rules/full/strategy.d.ts +4 -0
  120. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  121. package/dist/site-system/rules/full/strategy.js +19 -3
  122. package/dist/site-system/rules/full/strategy.js.map +1 -1
  123. package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
  124. package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
  125. package/dist/site-system/rules/full/w3-refresh.js +70 -0
  126. package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
  127. package/dist/site-system/rules/guards/index.d.ts +22 -0
  128. package/dist/site-system/rules/guards/index.d.ts.map +1 -0
  129. package/dist/site-system/rules/guards/index.js +109 -0
  130. package/dist/site-system/rules/guards/index.js.map +1 -0
  131. package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
  132. package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
  133. package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
  134. package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
  135. package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
  136. package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
  137. package/dist/site-system/rules/guards/w9-params.js +84 -0
  138. package/dist/site-system/rules/guards/w9-params.js.map +1 -0
  139. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  140. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  141. package/dist/site-system/rules/sections/k2-skin-coverage.js +6 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/runners.js +1 -1
  144. package/dist/site-system/runners.js.map +1 -1
  145. package/dist/site-system-e2e.d.ts +1 -0
  146. package/dist/site-system-e2e.d.ts.map +1 -1
  147. package/dist/site-system-e2e.js +8 -2
  148. package/dist/site-system-e2e.js.map +1 -1
  149. package/package.json +6 -4
@@ -1 +1 @@
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 { 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 { estatePlanProblems, type SiteEstatePlan } 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\";\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 const waves: { releaseOn: string; routes: string[]; kind?: \"release\" | \"return\" }[] = [];\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 const kind = wave[\"kind\"];\n if (kind !== undefined && kind !== \"release\" && kind !== \"return\") {\n problems.waves.push(`Wave ${index + 1} in ${FULL_STRATEGY_FILE} 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 const rawEstate = record?.[\"estate\"];\n if (rawEstate !== undefined) {\n if (!isLiteralObject(rawEstate) || !isFullyResolved(rawEstate) || !Array.isArray(rawEstate[\"indexedNow\"])) {\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.`);\n } else {\n estate = rawEstate as unknown as SiteEstatePlan;\n problems.waves.push(...estatePlanProblems(estate, waves, today).map((problem) => `${FULL_STRATEGY_FILE}: ${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 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"]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * W3 (full tier, 3.13.0; cost-aware additions §4): no estate family refreshes more often than every 7 days. Estate
3
+ * pages (suburb, service-in-suburb, region and council pages) use the Brandlab estate preset, `revalidate = false`,
4
+ * and are rewritten by deploys and by the reviews tag when their reviews change. A page that shows reviews follows
5
+ * the shared reviews fetch: daily by default (which fails here), weekly once the site adopts `refresh: 'on-change'`
6
+ * with the signed revalidate route. A family that genuinely needs a faster refresh is
7
+ * recorded in brandlab.config.ts `siteSystem.refreshExceptions` with the reason and a decision id.
8
+ *
9
+ * The rule reads what Next built: each page's `initialRevalidateSeconds` in `.next/prerender-manifest.json`, and the
10
+ * `fallbackRevalidate` of a dynamic route that renders more pages on demand. Findings are grouped by family and route,
11
+ * so a 5,600-page family is one finding, not thousands.
12
+ */
13
+ import type { SiteSystemFinding } from "../../checks.js";
14
+ import { type FullContext } from "./common.js";
15
+ /** The shortest timed refresh an estate page may have without a recorded exception: 7 days. */
16
+ export declare const W3_MIN_REVALIDATE_SECONDS = 604800;
17
+ /** The families W3 covers. */
18
+ export declare const W3_FAMILIES: readonly ["suburb", "service-in-suburb", "region", "council"];
19
+ /** "1 day", "12 hours", "30 minutes", "45 seconds": the largest whole unit. */
20
+ export declare function refreshInterval(seconds: number): string;
21
+ export declare function checkW3(context: FullContext): SiteSystemFinding[];
22
+ //# sourceMappingURL=w3-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"w3-refresh.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/w3-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAA4E,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzH,+FAA+F;AAC/F,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD,8BAA8B;AAC9B,eAAO,MAAM,WAAW,+DAA8B,CAAC;AAEvD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKvD;AAcD,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA2DjE"}
@@ -0,0 +1,70 @@
1
+ import { SITE_SYSTEM_ESTATE_FAMILIES } from "../../config.js";
2
+ import { FAMILY_LABELS, fail, needsBuild, notApplicable, outcome, plural, warning } from "./common.js";
3
+ /** The shortest timed refresh an estate page may have without a recorded exception: 7 days. */
4
+ export const W3_MIN_REVALIDATE_SECONDS = 604_800;
5
+ /** The families W3 covers. */
6
+ export const W3_FAMILIES = SITE_SYSTEM_ESTATE_FAMILIES;
7
+ /** "1 day", "12 hours", "30 minutes", "45 seconds": the largest whole unit. */
8
+ export function refreshInterval(seconds) {
9
+ for (const [size, unit] of [[86_400, "day"], [3_600, "hour"], [60, "minute"]]) {
10
+ if (seconds >= size && seconds % size === 0)
11
+ return plural(seconds / size, unit);
12
+ }
13
+ return plural(seconds, "second");
14
+ }
15
+ const label = (family) => FAMILY_LABELS[family] ?? `${family} page`;
16
+ export function checkW3(context) {
17
+ const skip = needsBuild("W3", context.estate);
18
+ if (skip)
19
+ return [skip];
20
+ const { estate } = context;
21
+ const exceptions = new Map((context.refreshExceptions ?? []).map((exception) => [exception.family, exception]));
22
+ const pages = estate.pages.filter((page) => W3_FAMILIES.includes(page.family ?? ""));
23
+ if (pages.length === 0)
24
+ return [notApplicable("W3", "The build has no estate pages (suburb, service-in-suburb, region or council).")];
25
+ const groups = new Map();
26
+ let timed = 0;
27
+ for (const page of pages) {
28
+ const route = page.srcRoute ?? page.path;
29
+ const key = `${page.family}|${route}`;
30
+ const group = groups.get(key) ?? { family: page.family, route, pages: 0, shortest: undefined, fallback: undefined };
31
+ group.pages += 1;
32
+ if (typeof page.revalidate === "number") {
33
+ timed += 1;
34
+ group.shortest = Math.min(group.shortest ?? Number.POSITIVE_INFINITY, page.revalidate);
35
+ }
36
+ const fallback = page.srcRoute ? estate.fallbackRevalidate.get(page.srcRoute) : undefined;
37
+ if (typeof fallback === "number")
38
+ group.fallback = fallback;
39
+ groups.set(key, group);
40
+ }
41
+ const findings = [];
42
+ const faster = new Set();
43
+ for (const group of groups.values()) {
44
+ const intervals = [group.shortest, group.fallback].filter((value) => value !== undefined);
45
+ if (intervals.length === 0)
46
+ continue;
47
+ const shortest = Math.min(...intervals);
48
+ if (shortest >= W3_MIN_REVALIDATE_SECONDS)
49
+ continue;
50
+ faster.add(group.family);
51
+ const what = group.shortest !== undefined && group.shortest === shortest
52
+ ? `${plural(group.pages, label(group.family))} from ${group.route} refresh every ${refreshInterval(shortest)}`
53
+ : `${label(group.family)}s that ${group.route} renders on demand refresh every ${refreshInterval(shortest)}`;
54
+ const exception = exceptions.get(group.family);
55
+ if (exception && shortest >= exception.revalidateSeconds) {
56
+ findings.push(warning("W3", group.route, `${what}, as the recorded exception allows (decision ${exception.decisionId}: ${exception.reason}).`));
57
+ continue;
58
+ }
59
+ const allowed = exception ? `; the recorded exception (decision ${exception.decisionId}) allows every ${refreshInterval(exception.revalidateSeconds)} at most` : "";
60
+ findings.push(fail("W3", group.route, `${what}, more often than every 7 days${allowed}. Use the Brandlab estate preset (export const revalidate = false; @brandlab-digital/site-next/estate-segment); where the pages show reviews, adopt refresh: 'on-change' in getBusinessProfileReviews with the signed reviews revalidate route; or record the family in siteSystem.refreshExceptions in brandlab.config.ts with the reason and decision id.`));
61
+ }
62
+ for (const exception of exceptions.values()) {
63
+ if (faster.has(exception.family))
64
+ continue;
65
+ findings.push(warning("W3", "brandlab.config.ts", `The refresh exception for ${label(exception.family)}s (decision ${exception.decisionId}) is no longer needed: no ${label(exception.family)} refreshes more often than every 7 days.`));
66
+ }
67
+ const never = pages.length - timed;
68
+ return outcome("W3", findings, `All ${plural(pages.length, "estate page")} refresh no more often than every 7 days${never > 0 ? ` (${never} never on a timer)` : ""}.`);
69
+ }
70
+ //# sourceMappingURL=w3-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"w3-refresh.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/w3-refresh.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * W3 (full tier, 3.13.0; cost-aware additions §4): no estate family refreshes more often than every 7 days. Estate\n * pages (suburb, service-in-suburb, region and council pages) use the Brandlab estate preset, `revalidate = false`,\n * and are rewritten by deploys and by the reviews tag when their reviews change. A page that shows reviews follows\n * the shared reviews fetch: daily by default (which fails here), weekly once the site adopts `refresh: 'on-change'`\n * with the signed revalidate route. A family that genuinely needs a faster refresh is\n * recorded in brandlab.config.ts `siteSystem.refreshExceptions` with the reason and a decision id.\n *\n * The rule reads what Next built: each page's `initialRevalidateSeconds` in `.next/prerender-manifest.json`, and the\n * `fallbackRevalidate` of a dynamic route that renders more pages on demand. Findings are grouped by family and route,\n * so a 5,600-page family is one finding, not thousands.\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { SITE_SYSTEM_ESTATE_FAMILIES, type SiteSystemRefreshException } from \"../../config.js\";\nimport { FAMILY_LABELS, fail, needsBuild, notApplicable, outcome, plural, warning, type FullContext } from \"./common.js\";\n\n/** The shortest timed refresh an estate page may have without a recorded exception: 7 days. */\nexport const W3_MIN_REVALIDATE_SECONDS = 604_800;\n\n/** The families W3 covers. */\nexport const W3_FAMILIES = SITE_SYSTEM_ESTATE_FAMILIES;\n\n/** \"1 day\", \"12 hours\", \"30 minutes\", \"45 seconds\": the largest whole unit. */\nexport function refreshInterval(seconds: number): string {\n for (const [size, unit] of [[86_400, \"day\"], [3_600, \"hour\"], [60, \"minute\"]] as const) {\n if (seconds >= size && seconds % size === 0) return plural(seconds / size, unit);\n }\n return plural(seconds, \"second\");\n}\n\nconst label = (family: string) => FAMILY_LABELS[family] ?? `${family} page`;\n\ninterface Group {\n readonly family: string;\n readonly route: string;\n pages: number;\n /** The shortest timed refresh of a built page in the group. */\n shortest: number | undefined;\n /** The route's on-demand pages' refresh, when it renders more on demand. */\n fallback: number | undefined;\n}\n\nexport function checkW3(context: FullContext): SiteSystemFinding[] {\n const skip = needsBuild(\"W3\", context.estate);\n if (skip) return [skip];\n const { estate } = context;\n const exceptions = new Map<string, SiteSystemRefreshException>((context.refreshExceptions ?? []).map((exception) => [exception.family, exception]));\n const pages = estate.pages.filter((page) => (W3_FAMILIES as readonly string[]).includes(page.family ?? \"\"));\n if (pages.length === 0) return [notApplicable(\"W3\", \"The build has no estate pages (suburb, service-in-suburb, region or council).\")];\n\n const groups = new Map<string, Group>();\n let timed = 0;\n for (const page of pages) {\n const route = page.srcRoute ?? page.path;\n const key = `${page.family}|${route}`;\n const group = groups.get(key) ?? { family: page.family!, route, pages: 0, shortest: undefined, fallback: undefined };\n group.pages += 1;\n if (typeof page.revalidate === \"number\") {\n timed += 1;\n group.shortest = Math.min(group.shortest ?? Number.POSITIVE_INFINITY, page.revalidate);\n }\n const fallback = page.srcRoute ? estate.fallbackRevalidate.get(page.srcRoute) : undefined;\n if (typeof fallback === \"number\") group.fallback = fallback;\n groups.set(key, group);\n }\n\n const findings: SiteSystemFinding[] = [];\n const faster = new Set<string>();\n for (const group of groups.values()) {\n const intervals = [group.shortest, group.fallback].filter((value): value is number => value !== undefined);\n if (intervals.length === 0) continue;\n const shortest = Math.min(...intervals);\n if (shortest >= W3_MIN_REVALIDATE_SECONDS) continue;\n faster.add(group.family);\n const what = group.shortest !== undefined && group.shortest === shortest\n ? `${plural(group.pages, label(group.family))} from ${group.route} refresh every ${refreshInterval(shortest)}`\n : `${label(group.family)}s that ${group.route} renders on demand refresh every ${refreshInterval(shortest)}`;\n const exception = exceptions.get(group.family);\n if (exception && shortest >= exception.revalidateSeconds) {\n findings.push(warning(\"W3\", group.route, `${what}, as the recorded exception allows (decision ${exception.decisionId}: ${exception.reason}).`));\n continue;\n }\n const allowed = exception ? `; the recorded exception (decision ${exception.decisionId}) allows every ${refreshInterval(exception.revalidateSeconds)} at most` : \"\";\n findings.push(\n fail(\n \"W3\",\n group.route,\n `${what}, more often than every 7 days${allowed}. Use the Brandlab estate preset (export const revalidate = false; @brandlab-digital/site-next/estate-segment); where the pages show reviews, adopt refresh: 'on-change' in getBusinessProfileReviews with the signed reviews revalidate route; or record the family in siteSystem.refreshExceptions in brandlab.config.ts with the reason and decision id.`,\n ),\n );\n }\n for (const exception of exceptions.values()) {\n if (faster.has(exception.family)) continue;\n findings.push(warning(\"W3\", \"brandlab.config.ts\", `The refresh exception for ${label(exception.family)}s (decision ${exception.decisionId}) is no longer needed: no ${label(exception.family)} refreshes more often than every 7 days.`));\n }\n const never = pages.length - timed;\n return outcome(\n \"W3\",\n findings,\n `All ${plural(pages.length, \"estate page\")} refresh no more often than every 7 days${never > 0 ? ` (${never} never on a timer)` : \"\"}.`,\n );\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ import type { SiteSystemFetch } from "../../samples.js";
3
+ import type { SiteSystemRenderedInput } from "../../runners.js";
4
+ export { checkW9Params, dynamicRoutePageFiles, searchParamLines } from "./w9-params.js";
5
+ export { checkW10, firewallRules, parseRobotsTxt, robotsAllows, robotsGroupsFor, userAgentConditionMatches, type FirewallRule, type RobotsGroup, type UserAgentCondition, type W10Input } from "./w10-crawlers.js";
6
+ export interface CostGuardRuleOptions {
7
+ /** The moment W5 judges a decision's reviewBy on (in Sydney). Defaults to now. */
8
+ readonly now?: Date;
9
+ /** Fetches robots.txt with --base-url (tests). */
10
+ readonly fetch?: SiteSystemFetch;
11
+ }
12
+ export declare function checkW5(projectRoot: string, today: string): SiteSystemFinding[];
13
+ /** True when robots.txt shuts every search and answer crawler out of the whole site (`Disallow: /`). */
14
+ export declare function robotsClosedToEveryCrawler(text: string): boolean;
15
+ /**
16
+ * The deployment environment a Next build was made for: the `NEXT_PUBLIC_VERCEL_ENV` that defineBrandlabNextConfig
17
+ * inlines (from VERCEL_ENV), as the build records it in required-server-files.json. Undefined when it is not recorded.
18
+ */
19
+ export declare function buildDeploymentEnvironment(buildDir: string): string | undefined;
20
+ /** Runs W5, W10 and W9-params. */
21
+ export declare function runCostGuardRules(input: SiteSystemRenderedInput, options?: CostGuardRuleOptions): Promise<readonly SiteSystemFinding[]>;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/guards/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAMhE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACxF,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,yBAAyB,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEnN,MAAM,WAAW,oBAAoB;IACnC,kFAAkF;IAClF,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;CAClC;AAMD,wBAAgB,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAO/E;AA0BD,wGAAwG;AACxG,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS/E;AAUD,kCAAkC;AAClC,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAwBjJ"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The cost guards (release 3.13.0): small checks so cost work never harms a site. W5, the origin region recorded and
3
+ * reviewed; W10, robots.txt and the firewall leave the search and answer crawlers open; W9-params, estate pages never
4
+ * read searchParams. The browser half of W9 (URL variants) is a journey (`defineBrandlabSiteSystemTests`).
5
+ */
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { originRegionProblems, sydneyToday } from "../../../origin-region.js";
9
+ import { parseProjectHosting, readSiteSystemProjectConfig } from "../../config.js";
10
+ import { fail, pass } from "../enquiry/common.js";
11
+ import { checkW9Params } from "./w9-params.js";
12
+ import { BRANDLAB_SEARCH_CRAWLERS } from "@brandlab-digital/site-core/site";
13
+ import { checkW10, parseRobotsTxt, robotsAllows } from "./w10-crawlers.js";
14
+ export { checkW9Params, dynamicRoutePageFiles, searchParamLines } from "./w9-params.js";
15
+ export { checkW10, firewallRules, parseRobotsTxt, robotsAllows, robotsGroupsFor, userAgentConditionMatches } from "./w10-crawlers.js";
16
+ function readOptional(path) {
17
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
18
+ }
19
+ export function checkW5(projectRoot, today) {
20
+ const config = readOptional(join(projectRoot, "brandlab.config.ts"));
21
+ const hosting = config === null ? undefined : parseProjectHosting(config);
22
+ const problems = originRegionProblems({ hosting, vercelJson: readOptional(join(projectRoot, "vercel.json")), today });
23
+ if (problems.length > 0)
24
+ return problems.map((problem) => fail("W5", problem.startsWith("vercel.json") ? "vercel.json" : "brandlab.config.ts", problem));
25
+ const regions = hosting?.state === "literal" ? JSON.stringify(hosting.value?.originRegion ?? "syd1") : '"syd1"';
26
+ return [pass("W5", `vercel.json runs the server functions in the recorded origin region (${regions}).`)];
27
+ }
28
+ async function readRobots(input, fetcher) {
29
+ if (input.baseUrl) {
30
+ const url = `${input.baseUrl.replace(/\/+$/, "")}/robots.txt`;
31
+ try {
32
+ const response = await (fetcher ?? globalThis.fetch)(url, { headers: { accept: "text/plain" }, redirect: "follow", signal: AbortSignal.timeout(30_000) });
33
+ if (!response.ok)
34
+ return { text: null, problem: `GET /robots.txt answered ${response.status}.` };
35
+ const closed = /\bnoindex\b/i.test(response.headers.get("x-robots-tag") ?? "");
36
+ return { text: await response.text(), closed };
37
+ }
38
+ catch (error) {
39
+ return { text: null, problem: `GET /robots.txt failed (${error instanceof Error ? error.message : String(error)}).` };
40
+ }
41
+ }
42
+ const buildDir = input.buildDir ?? join(input.projectRoot, ".next");
43
+ const file = join(buildDir, "server", "app", "robots.txt.body");
44
+ const text = readOptional(file);
45
+ if (text === null) {
46
+ return { text: null, problem: "The build has no prerendered robots.txt (.next/server/app/robots.txt.body); run next build, or pass --base-url to read it from the running site." };
47
+ }
48
+ // The robots helper closes every deployment but production on purpose: a build made for a Preview (VERCEL_ENV
49
+ // preview) is not judged. A production build, or one whose environment is not recorded, still is.
50
+ const environment = buildDeploymentEnvironment(buildDir);
51
+ return { text, closed: environment !== undefined && environment !== "production" && robotsClosedToEveryCrawler(text) };
52
+ }
53
+ /** True when robots.txt shuts every search and answer crawler out of the whole site (`Disallow: /`). */
54
+ export function robotsClosedToEveryCrawler(text) {
55
+ const groups = parseRobotsTxt(text);
56
+ return BRANDLAB_SEARCH_CRAWLERS.every((crawler) => !robotsAllows(groups, crawler, "/"));
57
+ }
58
+ /**
59
+ * The deployment environment a Next build was made for: the `NEXT_PUBLIC_VERCEL_ENV` that defineBrandlabNextConfig
60
+ * inlines (from VERCEL_ENV), as the build records it in required-server-files.json. Undefined when it is not recorded.
61
+ */
62
+ export function buildDeploymentEnvironment(buildDir) {
63
+ const manifest = readOptional(join(buildDir, "required-server-files.json"));
64
+ if (manifest === null)
65
+ return undefined;
66
+ try {
67
+ const value = JSON.parse(manifest).config?.env?.NEXT_PUBLIC_VERCEL_ENV;
68
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ function representativePaths(projectRoot) {
75
+ try {
76
+ return ["/", ...Object.values(readSiteSystemProjectConfig(projectRoot).representativeRoutes)];
77
+ }
78
+ catch {
79
+ return ["/"];
80
+ }
81
+ }
82
+ /** Runs W5, W10 and W9-params. */
83
+ export async function runCostGuardRules(input, options = {}) {
84
+ const today = sydneyToday(options.now);
85
+ const vercelText = readOptional(join(input.projectRoot, "vercel.json"));
86
+ let vercelJson = null;
87
+ try {
88
+ vercelJson = vercelText === null ? null : JSON.parse(vercelText);
89
+ }
90
+ catch {
91
+ vercelJson = null;
92
+ }
93
+ const config = readOptional(join(input.projectRoot, "brandlab.config.ts"));
94
+ const hosting = config === null ? undefined : parseProjectHosting(config);
95
+ const robots = await readRobots(input, options.fetch);
96
+ return [
97
+ ...checkW5(input.projectRoot, today),
98
+ ...checkW10({
99
+ robotsTxt: robots.text,
100
+ ...(robots.problem ? { robotsProblem: robots.problem } : {}),
101
+ closedDeployment: robots.closed === true,
102
+ paths: [...new Set(representativePaths(input.projectRoot))],
103
+ vercelJson,
104
+ hosting: hosting?.state === "literal" ? hosting.value : undefined,
105
+ }),
106
+ ...checkW9Params(input.projectRoot),
107
+ ];
108
+ }
109
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/guards/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The cost guards (release 3.13.0): small checks so cost work never harms a site. W5, the origin region recorded and\n * reviewed; W10, robots.txt and the firewall leave the search and answer crawlers open; W9-params, estate pages never\n * read searchParams. The browser half of W9 (URL variants) is a journey (`defineBrandlabSiteSystemTests`).\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { originRegionProblems, sydneyToday } from \"../../../origin-region.js\";\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { parseProjectHosting, readSiteSystemProjectConfig } from \"../../config.js\";\nimport type { SiteSystemFetch } from \"../../samples.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { fail, pass } from \"../enquiry/common.js\";\nimport { checkW9Params } from \"./w9-params.js\";\nimport { BRANDLAB_SEARCH_CRAWLERS } from \"@brandlab-digital/site-core/site\";\nimport { checkW10, parseRobotsTxt, robotsAllows } from \"./w10-crawlers.js\";\n\nexport { checkW9Params, dynamicRoutePageFiles, searchParamLines } from \"./w9-params.js\";\nexport { checkW10, firewallRules, parseRobotsTxt, robotsAllows, robotsGroupsFor, userAgentConditionMatches, type FirewallRule, type RobotsGroup, type UserAgentCondition, type W10Input } from \"./w10-crawlers.js\";\n\nexport interface CostGuardRuleOptions {\n /** The moment W5 judges a decision's reviewBy on (in Sydney). Defaults to now. */\n readonly now?: Date;\n /** Fetches robots.txt with --base-url (tests). */\n readonly fetch?: SiteSystemFetch;\n}\n\nfunction readOptional(path: string): string | null {\n return existsSync(path) ? readFileSync(path, \"utf8\") : null;\n}\n\nexport function checkW5(projectRoot: string, today: string): SiteSystemFinding[] {\n const config = readOptional(join(projectRoot, \"brandlab.config.ts\"));\n const hosting = config === null ? undefined : parseProjectHosting(config);\n const problems = originRegionProblems({ hosting, vercelJson: readOptional(join(projectRoot, \"vercel.json\")), today });\n if (problems.length > 0) return problems.map((problem) => fail(\"W5\", problem.startsWith(\"vercel.json\") ? \"vercel.json\" : \"brandlab.config.ts\", problem));\n const regions = hosting?.state === \"literal\" ? JSON.stringify((hosting.value as { originRegion?: unknown } | undefined)?.originRegion ?? \"syd1\") : '\"syd1\"';\n return [pass(\"W5\", `vercel.json runs the server functions in the recorded origin region (${regions}).`)];\n}\n\nasync function readRobots(input: SiteSystemRenderedInput, fetcher: SiteSystemFetch | undefined): Promise<{ text: string | null; problem?: string; closed?: boolean }> {\n if (input.baseUrl) {\n const url = `${input.baseUrl.replace(/\\/+$/, \"\")}/robots.txt`;\n try {\n const response = await (fetcher ?? (globalThis.fetch as unknown as SiteSystemFetch))(url, { headers: { accept: \"text/plain\" }, redirect: \"follow\", signal: AbortSignal.timeout(30_000) });\n if (!response.ok) return { text: null, problem: `GET /robots.txt answered ${response.status}.` };\n const closed = /\\bnoindex\\b/i.test(response.headers.get(\"x-robots-tag\") ?? \"\");\n return { text: await response.text(), closed };\n } catch (error) {\n return { text: null, problem: `GET /robots.txt failed (${error instanceof Error ? error.message : String(error)}).` };\n }\n }\n const buildDir = input.buildDir ?? join(input.projectRoot, \".next\");\n const file = join(buildDir, \"server\", \"app\", \"robots.txt.body\");\n const text = readOptional(file);\n if (text === null) {\n return { text: null, problem: \"The build has no prerendered robots.txt (.next/server/app/robots.txt.body); run next build, or pass --base-url to read it from the running site.\" };\n }\n // The robots helper closes every deployment but production on purpose: a build made for a Preview (VERCEL_ENV\n // preview) is not judged. A production build, or one whose environment is not recorded, still is.\n const environment = buildDeploymentEnvironment(buildDir);\n return { text, closed: environment !== undefined && environment !== \"production\" && robotsClosedToEveryCrawler(text) };\n}\n\n/** True when robots.txt shuts every search and answer crawler out of the whole site (`Disallow: /`). */\nexport function robotsClosedToEveryCrawler(text: string): boolean {\n const groups = parseRobotsTxt(text);\n return BRANDLAB_SEARCH_CRAWLERS.every((crawler) => !robotsAllows(groups, crawler, \"/\"));\n}\n\n/**\n * The deployment environment a Next build was made for: the `NEXT_PUBLIC_VERCEL_ENV` that defineBrandlabNextConfig\n * inlines (from VERCEL_ENV), as the build records it in required-server-files.json. Undefined when it is not recorded.\n */\nexport function buildDeploymentEnvironment(buildDir: string): string | undefined {\n const manifest = readOptional(join(buildDir, \"required-server-files.json\"));\n if (manifest === null) return undefined;\n try {\n const value = (JSON.parse(manifest) as { config?: { env?: Record<string, unknown> } }).config?.env?.NEXT_PUBLIC_VERCEL_ENV;\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction representativePaths(projectRoot: string): string[] {\n try {\n return [\"/\", ...Object.values(readSiteSystemProjectConfig(projectRoot).representativeRoutes)];\n } catch {\n return [\"/\"];\n }\n}\n\n/** Runs W5, W10 and W9-params. */\nexport async function runCostGuardRules(input: SiteSystemRenderedInput, options: CostGuardRuleOptions = {}): Promise<readonly SiteSystemFinding[]> {\n const today = sydneyToday(options.now);\n const vercelText = readOptional(join(input.projectRoot, \"vercel.json\"));\n let vercelJson: unknown = null;\n try {\n vercelJson = vercelText === null ? null : JSON.parse(vercelText);\n } catch {\n vercelJson = null;\n }\n const config = readOptional(join(input.projectRoot, \"brandlab.config.ts\"));\n const hosting = config === null ? undefined : parseProjectHosting(config);\n const robots = await readRobots(input, options.fetch);\n return [\n ...checkW5(input.projectRoot, today),\n ...checkW10({\n robotsTxt: robots.text,\n ...(robots.problem ? { robotsProblem: robots.problem } : {}),\n closedDeployment: robots.closed === true,\n paths: [...new Set(representativePaths(input.projectRoot))],\n vercelJson,\n hosting: hosting?.state === \"literal\" ? hosting.value : undefined,\n }),\n ...checkW9Params(input.projectRoot),\n ];\n}\n"]}
@@ -0,0 +1,55 @@
1
+ import type { SiteSystemFinding } from "../../checks.js";
2
+ export interface RobotsGroup {
3
+ /** The user-agent tokens, lower-cased. */
4
+ readonly agents: readonly string[];
5
+ readonly rules: readonly {
6
+ readonly allow: boolean;
7
+ readonly path: string;
8
+ }[];
9
+ readonly crawlDelay: string | null;
10
+ }
11
+ /** robots.txt as groups (RFC 9309): consecutive user-agent lines, then their rules. Comments and unknown lines are skipped. */
12
+ export declare function parseRobotsTxt(text: string): RobotsGroup[];
13
+ /** The groups that apply to a crawler: those naming its token, or else the `*` groups. */
14
+ export declare function robotsGroupsFor(groups: readonly RobotsGroup[], crawler: string): RobotsGroup[];
15
+ /** Whether a crawler may fetch `path` under these groups: the longest matching rule wins, and Allow wins a tie. */
16
+ export declare function robotsAllows(groups: readonly RobotsGroup[], crawler: string, path: string): boolean;
17
+ /** A user-agent condition from a firewall rule, in the forms Vercel and the brandlab.config record write. */
18
+ export type UserAgentCondition = {
19
+ readonly match: "contains" | "equals" | "regex";
20
+ readonly value: string;
21
+ } | {
22
+ readonly eq?: string;
23
+ readonly neq?: string;
24
+ readonly inc?: readonly string[];
25
+ readonly ninc?: readonly string[];
26
+ readonly pre?: string;
27
+ readonly suf?: string;
28
+ readonly re?: string;
29
+ };
30
+ /** Whether a user-agent condition matches the crawler (case-insensitive, so a doubtful rule is flagged). */
31
+ export declare function userAgentConditionMatches(condition: UserAgentCondition | string, crawler: string): boolean;
32
+ export interface FirewallRule {
33
+ /** Where the rule is recorded, for messages: `vercel.json routes[2]` or `hosting.firewall "block-scrapers"`. */
34
+ readonly source: string;
35
+ readonly action: string;
36
+ readonly userAgent: UserAgentCondition | string;
37
+ }
38
+ /** The firewall rules that match on the user agent: vercel.json `routes` with `mitigate`, and `hosting.firewall`. */
39
+ export declare function firewallRules(vercelJson: unknown, hosting: unknown): FirewallRule[];
40
+ export interface W10Input {
41
+ /** robots.txt text, or null when it could not be read; `robotsProblem` says why. */
42
+ readonly robotsTxt: string | null;
43
+ readonly robotsProblem?: string;
44
+ /**
45
+ * True when the robots.txt came from a Preview, closed on purpose: a running deployment that answers noindex, or a
46
+ * build made for a non-production environment whose robots.txt disallows everything.
47
+ */
48
+ readonly closedDeployment?: boolean;
49
+ /** Paths each crawler must be able to fetch: home and the representative routes. */
50
+ readonly paths: readonly string[];
51
+ readonly vercelJson: unknown;
52
+ readonly hosting: unknown;
53
+ }
54
+ export declare function checkW10(input: W10Input): SiteSystemFinding[];
55
+ //# sourceMappingURL=w10-crawlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"w10-crawlers.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/guards/w10-crawlers.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,+HAA+H;AAC/H,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CA6B1D;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAI9F;AAQD,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAUnG;AAED,6GAA6G;AAC7G,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAe7L,4GAA4G;AAC5G,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,kBAAkB,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAoB1G;AAED,MAAM,WAAW,YAAY;IAC3B,gHAAgH;IAChH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,kBAAkB,GAAG,MAAM,CAAC;CACjD;AAED,qHAAqH;AACrH,wBAAgB,aAAa,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,YAAY,EAAE,CAuBnF;AAID,MAAM,WAAW,QAAQ;IACvB,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,iBAAiB,EAAE,CA+B7D"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * W10, the crawler guard (3.13.0): robots.txt and the recorded firewall rules never block a search or answer crawler
3
+ * (site-core `BRANDLAB_SEARCH_CRAWLERS`), and robots.txt gives none of them a Crawl-delay. The robots.txt is the
4
+ * built one (or the running site's with --base-url); the firewall rules are vercel.json `routes` with `mitigate` and
5
+ * the rules recorded in brandlab.config.ts `hosting.firewall`.
6
+ */
7
+ import { BRANDLAB_SEARCH_CRAWLERS } from "@brandlab-digital/site-core/site";
8
+ import { fail, notApplicable, pass } from "../enquiry/common.js";
9
+ /** robots.txt as groups (RFC 9309): consecutive user-agent lines, then their rules. Comments and unknown lines are skipped. */
10
+ export function parseRobotsTxt(text) {
11
+ const groups = [];
12
+ let current = null;
13
+ let collectingAgents = false;
14
+ for (const raw of text.split(/\r?\n/)) {
15
+ const line = raw.replace(/#.*$/, "").trim();
16
+ const colon = line.indexOf(":");
17
+ if (colon === -1)
18
+ continue;
19
+ const key = line.slice(0, colon).trim().toLowerCase();
20
+ const value = line.slice(colon + 1).trim();
21
+ if (key === "user-agent") {
22
+ if (!collectingAgents || !current) {
23
+ current = { agents: [], rules: [], crawlDelay: null };
24
+ groups.push(current);
25
+ }
26
+ current.agents.push(value.toLowerCase());
27
+ collectingAgents = true;
28
+ continue;
29
+ }
30
+ if (!current)
31
+ continue;
32
+ collectingAgents = false;
33
+ if (key === "allow" || key === "disallow") {
34
+ // An empty Disallow allows everything; it adds no rule.
35
+ if (value)
36
+ current.rules.push({ allow: key === "allow", path: value });
37
+ }
38
+ else if (key === "crawl-delay") {
39
+ current.crawlDelay = value;
40
+ }
41
+ }
42
+ return groups;
43
+ }
44
+ /** The groups that apply to a crawler: those naming its token, or else the `*` groups. */
45
+ export function robotsGroupsFor(groups, crawler) {
46
+ const token = crawler.toLowerCase();
47
+ const named = groups.filter((group) => group.agents.includes(token));
48
+ return named.length > 0 ? named : groups.filter((group) => group.agents.includes("*"));
49
+ }
50
+ function patternMatches(pattern, path) {
51
+ const anchored = pattern.endsWith("$");
52
+ const body = (anchored ? pattern.slice(0, -1) : pattern).split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
53
+ return new RegExp(`^${body}${anchored ? "$" : ""}`).test(path);
54
+ }
55
+ /** Whether a crawler may fetch `path` under these groups: the longest matching rule wins, and Allow wins a tie. */
56
+ export function robotsAllows(groups, crawler, path) {
57
+ let best = null;
58
+ for (const group of robotsGroupsFor(groups, crawler)) {
59
+ for (const rule of group.rules) {
60
+ if (!patternMatches(rule.path, path))
61
+ continue;
62
+ const length = rule.path.length;
63
+ if (!best || length > best.length || (length === best.length && rule.allow))
64
+ best = { allow: rule.allow, length };
65
+ }
66
+ }
67
+ return best ? best.allow : true;
68
+ }
69
+ /** The user agents a crawler sends, as a rule would see them: its bare token and a typical full string. */
70
+ function crawlerUserAgents(crawler) {
71
+ return [crawler, `Mozilla/5.0 (compatible; ${crawler}/1.0)`];
72
+ }
73
+ function safeRegExp(source) {
74
+ try {
75
+ return new RegExp(source, "i");
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /** Whether a user-agent condition matches the crawler (case-insensitive, so a doubtful rule is flagged). */
82
+ export function userAgentConditionMatches(condition, crawler) {
83
+ return crawlerUserAgents(crawler).some((agent) => {
84
+ const ua = agent.toLowerCase();
85
+ if (typeof condition === "string")
86
+ return safeRegExp(condition)?.test(agent) ?? false;
87
+ if ("match" in condition) {
88
+ const value = condition.value.toLowerCase();
89
+ if (condition.match === "contains")
90
+ return ua.includes(value);
91
+ if (condition.match === "equals")
92
+ return ua === value;
93
+ return safeRegExp(condition.value)?.test(agent) ?? false;
94
+ }
95
+ const checks = [];
96
+ if (condition.eq !== undefined)
97
+ checks.push(ua === condition.eq.toLowerCase());
98
+ if (condition.neq !== undefined)
99
+ checks.push(ua !== condition.neq.toLowerCase());
100
+ if (condition.inc !== undefined)
101
+ checks.push(condition.inc.some((value) => ua === value.toLowerCase()));
102
+ if (condition.ninc !== undefined)
103
+ checks.push(!condition.ninc.some((value) => ua === value.toLowerCase()));
104
+ if (condition.pre !== undefined)
105
+ checks.push(ua.startsWith(condition.pre.toLowerCase()));
106
+ if (condition.suf !== undefined)
107
+ checks.push(ua.endsWith(condition.suf.toLowerCase()));
108
+ if (condition.re !== undefined)
109
+ checks.push(safeRegExp(condition.re)?.test(agent) ?? false);
110
+ return checks.length > 0 && checks.every(Boolean);
111
+ });
112
+ }
113
+ /** The firewall rules that match on the user agent: vercel.json `routes` with `mitigate`, and `hosting.firewall`. */
114
+ export function firewallRules(vercelJson, hosting) {
115
+ const rules = [];
116
+ const routes = isRecord(vercelJson) && Array.isArray(vercelJson.routes) ? vercelJson.routes : [];
117
+ routes.forEach((route, index) => {
118
+ if (!isRecord(route) || !isRecord(route.mitigate) || typeof route.mitigate.action !== "string")
119
+ return;
120
+ const has = Array.isArray(route.has) ? route.has : [];
121
+ for (const condition of has) {
122
+ if (!isRecord(condition) || condition.type !== "header" || String(condition.key).toLowerCase() !== "user-agent")
123
+ continue;
124
+ const value = condition.value;
125
+ if (typeof value === "string" || isRecord(value)) {
126
+ rules.push({ source: `vercel.json routes[${index}]`, action: route.mitigate.action, userAgent: value });
127
+ }
128
+ }
129
+ });
130
+ const recorded = isRecord(hosting) && Array.isArray(hosting.firewall) ? hosting.firewall : [];
131
+ for (const rule of recorded) {
132
+ if (!isRecord(rule) || !isRecord(rule.userAgent) || typeof rule.action !== "string")
133
+ continue;
134
+ const { match, value } = rule.userAgent;
135
+ if ((match === "contains" || match === "equals" || match === "regex") && typeof value === "string") {
136
+ rules.push({ source: `hosting.firewall "${String(rule.id)}"`, action: rule.action, userAgent: { match, value } });
137
+ }
138
+ }
139
+ return rules;
140
+ }
141
+ const BLOCKING_ACTIONS = new Set(["deny", "challenge"]);
142
+ export function checkW10(input) {
143
+ const findings = [];
144
+ const crawlers = BRANDLAB_SEARCH_CRAWLERS;
145
+ if (input.closedDeployment) {
146
+ findings.push(notApplicable("W10", "robots.txt came from a Preview deployment or a build made for one, which stays closed to every crawler; W10 reads the production build (VERCEL_ENV=production next build, or --base-url on production)."));
147
+ }
148
+ else if (input.robotsTxt === null) {
149
+ findings.push(fail("W10", "/robots.txt", input.robotsProblem ?? "robots.txt could not be read, so the crawler guard could not check it."));
150
+ }
151
+ else {
152
+ const groups = parseRobotsTxt(input.robotsTxt);
153
+ for (const crawler of crawlers) {
154
+ const blocked = input.paths.filter((path) => !robotsAllows(groups, crawler, path));
155
+ if (blocked.length > 0) {
156
+ findings.push(fail("W10", "/robots.txt", `robots.txt blocks ${crawler} from ${blocked.join(", ")}; search and answer crawlers stay allowed (no blanket robots rule).`));
157
+ }
158
+ const delay = robotsGroupsFor(groups, crawler).find((group) => group.crawlDelay !== null);
159
+ if (delay) {
160
+ findings.push(fail("W10", "/robots.txt", `robots.txt gives ${crawler} a Crawl-delay of ${delay.crawlDelay}; Google ignores it and Bing honours it, so it only slows Bing indexing. Keep one only with a recorded decision and a Romie-owned W10 exception.`));
161
+ }
162
+ }
163
+ }
164
+ for (const rule of firewallRules(input.vercelJson, input.hosting)) {
165
+ const hit = crawlers.filter((crawler) => userAgentConditionMatches(rule.userAgent, crawler));
166
+ if (hit.length === 0)
167
+ continue;
168
+ if (BLOCKING_ACTIONS.has(rule.action)) {
169
+ findings.push(fail("W10", rule.source.startsWith("vercel.json") ? "vercel.json" : "brandlab.config.ts", `The firewall rule ${rule.source} (${rule.action}) matches ${hit.join(", ")}; search and answer crawlers must not be denied or challenged.`));
170
+ }
171
+ else if (rule.action === "rate-limit") {
172
+ findings.push({ checkId: "W10", status: "pass", severity: "warning", target: "brandlab.config.ts", message: `The firewall rule ${rule.source} rate-limits ${hit.join(", ")}; check it never stops their normal crawl.` });
173
+ }
174
+ }
175
+ if (findings.some((finding) => finding.status === "fail" || finding.status === "not-applicable"))
176
+ return findings;
177
+ return [...findings, pass("W10", `robots.txt and the recorded firewall rules leave ${crawlers.join(", ")} open on ${input.paths.join(", ")}, with no Crawl-delay.`)];
178
+ }
179
+ function isRecord(value) {
180
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
181
+ }
182
+ //# sourceMappingURL=w10-crawlers.js.map