@koda-sl/baker-cli 0.557.0 → 0.558.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{account-reads-T6GTSAXE.js → account-reads-GQWTMLTY.js} +2 -2
- package/dist/{accounts-HJAQTVDA.js → accounts-ZFQYZT3J.js} +7 -2
- package/dist/accounts-ZFQYZT3J.js.map +1 -0
- package/dist/{ads-WWZK7UYG.js → ads-VYMFRGAC.js} +2 -2
- package/dist/{advertisers-FME2ABRB.js → advertisers-A3Q53R2S.js} +2 -2
- package/dist/{autocomplete-CBOYGT73.js → autocomplete-OOLUPUIJ.js} +2 -2
- package/dist/{changes-OIVPZJHE.js → changes-MVCX3GLE.js} +16 -11
- package/dist/changes-MVCX3GLE.js.map +1 -0
- package/dist/{chunk-JPK34QPS.js → chunk-2L24FULD.js} +3 -3
- package/dist/chunk-2L24FULD.js.map +1 -0
- package/dist/{chunk-PQ7RLE43.js → chunk-HNELOMY2.js} +26 -433
- package/dist/chunk-HNELOMY2.js.map +1 -0
- package/dist/chunk-NPGPRZW2.js +1313 -0
- package/dist/chunk-NPGPRZW2.js.map +1 -0
- package/dist/{chunk-SNWFM2DB.js → chunk-Q2JDS6WL.js} +11 -1
- package/dist/chunk-Q2JDS6WL.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{conversion-upload-command-J2FIO74Y.js → conversion-upload-command-U36QYHUT.js} +2 -2
- package/dist/{currency-B5T7TYKD.js → currency-XFY2EX72.js} +3 -3
- package/dist/{discover-SS3PO5Y6.js → discover-J5344ATV.js} +2 -2
- package/dist/{draft-EB5ZFDVN.js → draft-OIGJUFOH.js} +2 -2
- package/dist/{forecast-XDRKCR65.js → forecast-HFCRTFIF.js} +2 -2
- package/dist/{google-UJ4YJGTJ.js → google-TOO3FDZ6.js} +22 -22
- package/dist/{intent-OMSJPOJZ.js → intent-PU3XUDLL.js} +2 -2
- package/dist/{keyword-gap-3P72T4NV.js → keyword-gap-B6T7ZFTZ.js} +2 -2
- package/dist/{keyword-metrics-DDY2RDYK.js → keyword-metrics-CXGNLERO.js} +2 -2
- package/dist/{keywords-for-site-TFHOM23P.js → keywords-for-site-OYMDRMMX.js} +2 -2
- package/dist/{languages-WWKMRZCH.js → languages-ETKG5WM3.js} +2 -2
- package/dist/{locations-P3CUBKYZ.js → locations-VPZYTFVK.js} +2 -2
- package/dist/{metrics-PSXIV4QS.js → metrics-J3WD2CUD.js} +2 -2
- package/dist/{query-PC7V3OFF.js → query-ZYG5RCCW.js} +5 -5
- package/dist/{relevant-pages-3CCAQA5Y.js → relevant-pages-7U6I2WPM.js} +2 -2
- package/dist/{research-M6LXE3VX.js → research-3BWJCS7N.js} +8 -8
- package/dist/{themes-BGBDYQQK.js → themes-C4PPU427.js} +2 -2
- package/dist/{url-options-GJ7ZIUPD.js → url-options-C3ZOWN2L.js} +3 -3
- package/package.json +1 -1
- package/dist/accounts-HJAQTVDA.js.map +0 -1
- package/dist/changes-OIVPZJHE.js.map +0 -1
- package/dist/chunk-6HPTF5YS.js +0 -857
- package/dist/chunk-6HPTF5YS.js.map +0 -1
- package/dist/chunk-JPK34QPS.js.map +0 -1
- package/dist/chunk-PQ7RLE43.js.map +0 -1
- package/dist/chunk-SNWFM2DB.js.map +0 -1
- /package/dist/{account-reads-T6GTSAXE.js.map → account-reads-GQWTMLTY.js.map} +0 -0
- /package/dist/{ads-WWZK7UYG.js.map → ads-VYMFRGAC.js.map} +0 -0
- /package/dist/{advertisers-FME2ABRB.js.map → advertisers-A3Q53R2S.js.map} +0 -0
- /package/dist/{autocomplete-CBOYGT73.js.map → autocomplete-OOLUPUIJ.js.map} +0 -0
- /package/dist/{conversion-upload-command-J2FIO74Y.js.map → conversion-upload-command-U36QYHUT.js.map} +0 -0
- /package/dist/{currency-B5T7TYKD.js.map → currency-XFY2EX72.js.map} +0 -0
- /package/dist/{discover-SS3PO5Y6.js.map → discover-J5344ATV.js.map} +0 -0
- /package/dist/{draft-EB5ZFDVN.js.map → draft-OIGJUFOH.js.map} +0 -0
- /package/dist/{forecast-XDRKCR65.js.map → forecast-HFCRTFIF.js.map} +0 -0
- /package/dist/{google-UJ4YJGTJ.js.map → google-TOO3FDZ6.js.map} +0 -0
- /package/dist/{intent-OMSJPOJZ.js.map → intent-PU3XUDLL.js.map} +0 -0
- /package/dist/{keyword-gap-3P72T4NV.js.map → keyword-gap-B6T7ZFTZ.js.map} +0 -0
- /package/dist/{keyword-metrics-DDY2RDYK.js.map → keyword-metrics-CXGNLERO.js.map} +0 -0
- /package/dist/{keywords-for-site-TFHOM23P.js.map → keywords-for-site-OYMDRMMX.js.map} +0 -0
- /package/dist/{languages-WWKMRZCH.js.map → languages-ETKG5WM3.js.map} +0 -0
- /package/dist/{locations-P3CUBKYZ.js.map → locations-VPZYTFVK.js.map} +0 -0
- /package/dist/{metrics-PSXIV4QS.js.map → metrics-J3WD2CUD.js.map} +0 -0
- /package/dist/{query-PC7V3OFF.js.map → query-ZYG5RCCW.js.map} +0 -0
- /package/dist/{relevant-pages-3CCAQA5Y.js.map → relevant-pages-7U6I2WPM.js.map} +0 -0
- /package/dist/{research-M6LXE3VX.js.map → research-3BWJCS7N.js.map} +0 -0
- /package/dist/{themes-BGBDYQQK.js.map → themes-C4PPU427.js.map} +0 -0
- /package/dist/{url-options-GJ7ZIUPD.js.map → url-options-C3ZOWN2L.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/ads/google/during-literals.ts","../src/commands/ads/google/changes-window.ts","../src/commands/ads/google/list-fields.ts","../src/commands/ads/google/correction-table.ts","../src/commands/ads/google/error-parser.ts"],"sourcesContent":["/**\n * The date literals GAQL's `DURING` accepts — Google's grammar, verbatim. Anything else is refused by\n * Google with \"Invalid date literal supplied for DURING operator\", so it is refused here first.\n * https://developers.google.com/google-ads/api/docs/query/grammar\n */\nexport const GAQL_DURING_LITERALS = [\n \"TODAY\",\n \"YESTERDAY\",\n \"LAST_7_DAYS\",\n \"LAST_14_DAYS\",\n \"LAST_30_DAYS\",\n \"LAST_BUSINESS_WEEK\",\n \"THIS_MONTH\",\n \"LAST_MONTH\",\n \"THIS_WEEK_SUN_TODAY\",\n \"THIS_WEEK_MON_TODAY\",\n \"LAST_WEEK_SUN_SAT\",\n \"LAST_WEEK_MON_SUN\",\n] as const;\n\nexport function isDuringLiteral(value: string): boolean {\n return (GAQL_DURING_LITERALS as readonly string[]).includes(value.toUpperCase());\n}\n\nfunction isoDay(date: Date): string {\n return date.toISOString().slice(0, 10);\n}\n\nfunction utcDate(year: number, month: number, day: number): Date {\n return new Date(Date.UTC(year, month, day));\n}\n\n/** `months` calendar months before `date`, on the same day or the target month's last day if shorter. */\nfunction monthsBefore(date: Date, months: number): Date {\n const lastDayOfTarget = utcDate(date.getUTCFullYear(), date.getUTCMonth() - months + 1, 0).getUTCDate();\n return utcDate(date.getUTCFullYear(), date.getUTCMonth() - months, Math.min(date.getUTCDate(), lastDayOfTarget));\n}\n\n/**\n * The BETWEEN range an unsupported literal means, or null when it names no fixed window (ALL_TIME).\n * Rolling windows end yesterday, as Google's own LAST_30_DAYS does; calendar periods run to their end,\n * or to today for the current one.\n */\nexport function equivalentBetween(literal: string, now: Date): string | null {\n const upper = literal.toUpperCase();\n const today = utcDate(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());\n const yesterday = utcDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() - 1);\n const range = (from: Date, to: Date) => `BETWEEN '${isoDay(from)}' AND '${isoDay(to)}'`;\n const year = today.getUTCFullYear();\n const quarterStart = Math.floor(today.getUTCMonth() / 3) * 3;\n\n const rolling = upper.match(/^LAST_(\\d+)_(DAYS|WEEKS|MONTHS|YEARS)$/);\n if (rolling) {\n const n = Number(rolling[1]);\n const unit = rolling[2];\n if (n < 1) return null;\n if (unit === \"DAYS\" || unit === \"WEEKS\") {\n const days = unit === \"DAYS\" ? n : n * 7;\n return range(utcDate(year, today.getUTCMonth(), today.getUTCDate() - days), yesterday);\n }\n const months = unit === \"MONTHS\" ? n : n * 12;\n return range(monthsBefore(today, months), yesterday);\n }\n\n switch (upper) {\n case \"THIS_YEAR\":\n return range(utcDate(year, 0, 1), today);\n case \"LAST_YEAR\":\n return range(utcDate(year - 1, 0, 1), utcDate(year - 1, 11, 31));\n case \"THIS_QUARTER\":\n return range(utcDate(year, quarterStart, 1), today);\n case \"LAST_QUARTER\":\n return range(utcDate(year, quarterStart - 3, 1), utcDate(year, quarterStart, 0));\n default:\n return null;\n }\n}\n\nexport function duringLiteralExplanation(literal: string, between: string | null): string {\n const accepted = GAQL_DURING_LITERALS.join(\", \");\n const lead = between\n ? `Google does not accept ${literal} after DURING. The same window as explicit dates is ${between}.`\n : `Google does not accept ${literal} after DURING, and it names no fixed window — use BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD', or drop the date filter.`;\n return `${lead} DURING accepts only: ${accepted}. Any other window needs BETWEEN.`;\n}\n","import {\n CHANGE_SCOPE_MAX_DAYS,\n CHANGE_SCOPES,\n type ChangeScope,\n DEFAULT_CHANGE_DAYS,\n isChangeLookbackWithinReach,\n} from \"@baker/api/ads-google\";\nimport type { QueryFix } from \"../types.ts\";\nimport { equivalentBetween } from \"./during-literals.ts\";\n\nexport { CHANGE_SCOPE_MAX_DAYS, DEFAULT_CHANGE_DAYS };\n\n/**\n * A year of monthly spend per campaign — what to read when a change is older than any history\n * resource reaches. Dates are spelled out: GAQL has no LAST_12_MONTHS literal.\n */\nexport function monthlySpendTrendQuery(now: Date = new Date()): string {\n return `SELECT campaign.name, segments.month, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date ${equivalentBetween(\"LAST_12_MONTHS\", now)}`;\n}\n\nexport type ChangesWindowResolution =\n | { ok: true; scope: ChangeScope; days: number; hints: string[] }\n | { ok: false; error: { code: string; message: string; fix: QueryFix } };\n\nfunction isChangeScope(value: string): value is ChangeScope {\n return (CHANGE_SCOPES as readonly string[]).includes(value);\n}\n\nexport function resolveChangesWindow(input: { days?: number; scope?: string }): ChangesWindowResolution {\n const scope = input.scope ?? \"detail\";\n if (!isChangeScope(scope)) {\n return {\n ok: false,\n error: {\n code: \"INVALID_SCOPE\",\n message: `Unknown scope '${scope}'. Use 'detail' (field-level changes, 30 days) or 'summary' (which resources changed, 90 days).`,\n fix: {\n action: \"retry_with_flag\",\n explanation: \"Pass --scope detail or --scope summary.\",\n },\n },\n };\n }\n\n const days = input.days ?? DEFAULT_CHANGE_DAYS;\n if (!Number.isInteger(days) || days < 1) {\n return {\n ok: false,\n error: {\n code: \"INVALID_LOOKBACK\",\n message: `--days must be a whole number of days, got '${days}'.`,\n fix: {\n action: \"retry_with_flag\",\n explanation: `Pass --days as a positive integer, e.g. --days ${DEFAULT_CHANGE_DAYS}.`,\n },\n },\n };\n }\n\n if (!isChangeLookbackWithinReach(days, scope)) {\n return { ok: false, error: lookbackTooLong(scope, days, CHANGE_SCOPE_MAX_DAYS[scope]) };\n }\n\n return { ok: true, scope, days, hints: hintsFor(scope, days) };\n}\n\nfunction lookbackTooLong(\n scope: ChangeScope,\n days: number,\n max: number,\n): {\n code: string;\n message: string;\n fix: QueryFix;\n} {\n if (scope === \"detail\" && days <= CHANGE_SCOPE_MAX_DAYS.summary) {\n return {\n code: \"LOOKBACK_TOO_LONG\",\n message: `Google keeps field-level change detail for ${max} days only; ${days} days is outside that window.`,\n fix: {\n action: \"narrow_date_range\",\n explanation: `Re-run with --scope summary --days ${days} to reach back ${CHANGE_SCOPE_MAX_DAYS.summary} days. Summary tells you which campaigns, ad groups, ads and criteria changed and when, but not the old and new values — read the resource itself for current values.`,\n },\n };\n }\n\n return {\n code: \"LOOKBACK_TOO_LONG\",\n message: `Google's account history API reaches back ${CHANGE_SCOPE_MAX_DAYS.summary} days at most; ${days} days is outside that window.`,\n fix: {\n action: \"use_different_resource\",\n explanation: `Do not abort — infer the change from performance instead. Query monthly metrics per campaign (${monthlySpendTrendQuery()}) to see which campaigns stopped or started spending. That shows what changed and when, though not who changed it. Older history exists only in the Google Ads web UI, which has no API — ask the user to check it if the 'who' matters.`,\n },\n };\n}\n\nfunction hintsFor(scope: ChangeScope, days: number): string[] {\n const hints: string[] = [];\n\n if (scope === \"detail\") {\n hints.push(\n `Detailed history covers the last ${CHANGE_SCOPE_MAX_DAYS.detail} days. For anything older use --scope summary (reaches ${CHANGE_SCOPE_MAX_DAYS.summary} days, reports which resources changed but not their values).`,\n );\n } else {\n hints.push(\n \"Summary history reports which resources changed and when, not old and new values. To see what a changed resource looks like now, query it by resource_name.\",\n );\n if (days > CHANGE_SCOPE_MAX_DAYS.detail) {\n hints.push(\n `Changes inside the last ${CHANGE_SCOPE_MAX_DAYS.detail} days also have field-level detail — re-run with --scope detail for those.`,\n );\n }\n }\n\n return hints;\n}\n\n/** The most change events one `changes` read returns (the route's own cap). */\nexport const CHANGES_MAX_LIMIT = 1000;\n\nconst CHANGE_TIME_FIELDS = [\"change_event.change_date_time\", \"change_status.last_change_date_time\"];\n\n/**\n * A read that came back exactly at --limit has, as far as anyone can tell, more\n * behind it: Google returns the newest first, so the window the caller asked for\n * is covered only back to the oldest row. Said in words, because a capped read\n * looks like a complete one and \"nobody touched it in 30 days\" is the conclusion\n * an agent draws from it.\n */\nexport function cappedChangesHint(rows: Array<Record<string, unknown>>, limit: number, days: number): string | null {\n if (rows.length < limit) return null;\n const oldest = rows[rows.length - 1];\n const at = CHANGE_TIME_FIELDS.map((field) => oldest?.[field]).find((value) => typeof value === \"string\");\n const reached = typeof at === \"string\" ? `reaching back only to ${at}` : \"not the whole window\";\n const more =\n limit < CHANGES_MAX_LIMIT\n ? `re-run with --limit ${Math.min(limit * 2, CHANGES_MAX_LIMIT)}, or narrow with --resource-type`\n : \"narrow with --resource-type\";\n return `This read came back full at --limit ${limit}, ${reached}, not the ${days} days asked for: older changes almost certainly exist and are NOT shown. Do not conclude nothing changed before then — ${more}.`;\n}\n","// Every filterable list (repeated) field in Google Ads API v25 — 92 of them. Google filters a list\n// field only with CONTAINS ALL / ANY / NONE and refuses anything else (=, IN, LIKE, IS NULL …) with\n// OPERATOR_FIELD_MISMATCH; 88 were probed live on v23 and each answered exactly that, and v25 added\n// the four `lift_measurement_config.*` lists (regenerated 2026-10-08, none removed). Regenerate\n// on an API version bump (list-fields.test.ts fails until you do) from GoogleAdsFieldService:\n// POST /v<N>/googleAdsFields:search\n// {\"query\": \"SELECT name WHERE is_repeated = true AND filterable = true\"}\nexport const LIST_FIELDS_API_VERSION = \"v25\";\n\nexport const LIST_FIELDS: ReadonlySet<string> = new Set([\n \"ad.final_mobile_urls\",\n \"ad.final_urls\",\n \"ad_group.excluded_parent_asset_field_types\",\n \"ad_group.excluded_parent_asset_set_types\",\n \"ad_group.labels\",\n \"ad_group.primary_status_reasons\",\n \"ad_group_ad.ad.final_mobile_urls\",\n \"ad_group_ad.ad.final_urls\",\n \"ad_group_ad.labels\",\n \"ad_group_ad.primary_status_reasons\",\n \"ad_group_asset.primary_status_reasons\",\n \"ad_group_criterion.disapproval_reasons\",\n \"ad_group_criterion.final_mobile_urls\",\n \"ad_group_criterion.final_urls\",\n \"ad_group_criterion.labels\",\n \"ad_group_criterion.primary_status_reasons\",\n \"ad_group_criterion.topic.path\",\n \"ad_group_criterion.webpage.sample.sample_urls\",\n \"asset.dynamic_custom_asset.contextual_keywords\",\n \"asset.dynamic_custom_asset.similar_ids\",\n \"asset.dynamic_education_asset.contextual_keywords\",\n \"asset.dynamic_education_asset.similar_program_ids\",\n \"asset.dynamic_flights_asset.similar_destination_ids\",\n \"asset.dynamic_hotels_and_rentals_asset.contextual_keywords\",\n \"asset.dynamic_hotels_and_rentals_asset.similar_property_ids\",\n \"asset.dynamic_jobs_asset.contextual_keywords\",\n \"asset.dynamic_jobs_asset.similar_job_ids\",\n \"asset.dynamic_local_asset.contextual_keywords\",\n \"asset.dynamic_local_asset.similar_deal_ids\",\n \"asset.dynamic_real_estate_asset.contextual_keywords\",\n \"asset.dynamic_real_estate_asset.similar_listing_ids\",\n \"asset.dynamic_travel_asset.contextual_keywords\",\n \"asset.dynamic_travel_asset.similar_destination_ids\",\n \"asset.final_mobile_urls\",\n \"asset.final_urls\",\n \"asset.page_feed_asset.labels\",\n \"asset.structured_snippet_asset.values\",\n \"asset_group.final_mobile_urls\",\n \"asset_group.final_urls\",\n \"asset_group.primary_status_reasons\",\n \"asset_group_asset.primary_status_reasons\",\n \"asset_group_signal.disapproval_reasons\",\n \"asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.label_filters\",\n \"asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.listing_id_filters\",\n \"asset_set.location_set.business_profile_location_set.label_filters\",\n \"asset_set.location_set.business_profile_location_set.listing_id_filters\",\n \"bidding_data_exclusion.advertising_channel_types\",\n \"bidding_data_exclusion.campaigns\",\n \"bidding_data_exclusion.devices\",\n \"bidding_seasonality_adjustment.advertising_channel_types\",\n \"bidding_seasonality_adjustment.campaigns\",\n \"bidding_seasonality_adjustment.devices\",\n \"campaign.excluded_parent_asset_field_types\",\n \"campaign.excluded_parent_asset_set_types\",\n \"campaign.feed_types\",\n \"campaign.labels\",\n \"campaign.optimization_goal_setting.optimization_goal_types\",\n \"campaign.primary_status_reasons\",\n \"campaign.selective_optimization.conversion_actions\",\n \"campaign.shopping_setting.advertising_partner_ids\",\n \"campaign.text_guidelines.term_exclusions\",\n \"campaign_asset.primary_status_reasons\",\n \"campaign_criterion.location_group.geo_target_constants\",\n \"campaign_criterion.topic.path\",\n \"campaign_criterion.webpage.sample.sample_urls\",\n \"conversion_value_rule.audience_condition.user_interests\",\n \"conversion_value_rule.audience_condition.user_lists\",\n \"conversion_value_rule.device_condition.device_types\",\n \"conversion_value_rule.geo_location_condition.excluded_geo_target_constants\",\n \"conversion_value_rule.geo_location_condition.geo_target_constants\",\n \"conversion_value_rule_set.conversion_value_rules\",\n \"conversion_value_rule_set.dimensions\",\n \"custom_conversion_goal.conversion_actions\",\n \"customer.pay_per_conversion_eligibility_failure_reasons\",\n \"customer_asset.primary_status_reasons\",\n \"customer_client.applied_labels\",\n \"experiment_arm.campaigns\",\n \"experiment_arm.in_design_campaigns\",\n \"keyword_plan_campaign.language_constants\",\n \"lift_measurement_config.campaigns\",\n \"lift_measurement_config.conversion_actions\",\n \"lift_measurement_config.single_measurement_question_set.competitor_choices\",\n \"lift_measurement_config.single_measurement_question_set.question_measurements\",\n \"local_services_employee.category_ids\",\n \"local_services_employee.languages_spoken\",\n \"metrics.interaction_event_types\",\n \"recommendation.campaigns\",\n \"shared_criterion.webpage.sample.sample_urls\",\n \"shopping_product.target_countries\",\n \"topic_constant.path\",\n \"user_list.lookalike_user_list.country_codes\",\n \"user_list.lookalike_user_list.seed_user_list_ids\",\n]);\n\nexport const LIST_FIELD_OPERATORS = \"CONTAINS ALL, CONTAINS ANY, CONTAINS NONE\";\n\n/** Why a list field refused an operator, and the read that works instead. */\nexport function listFieldGuidance(field: string): string {\n return `${field} is a list field: it takes only ${LIST_FIELD_OPERATORS} — no comparison (=, !=, IN), pattern match or null test. To find rows that have it, drop that condition, keep ${field} in SELECT and filter the rows (rows without it omit the key); to match values, use ${field} CONTAINS ANY ('value')`;\n}\n","import type { CorrectionContext, CorrectionRule } from \"../types.ts\";\nimport { monthlySpendTrendQuery } from \"./changes-window.ts\";\nimport { listFieldGuidance } from \"./list-fields.ts\";\n\nfunction buildCommand(query: string, ctx: CorrectionContext): string {\n return `baker ads google query \"${query}\" --customer-id ${ctx.customerId}`;\n}\n\nexport const CORRECTION_RULES: CorrectionRule[] = [\n // 0. EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE — some resources (e.g. campaign_budget)\n // require fields filtered in WHERE to also be selected. The error names the exact field,\n // so add it to SELECT and retry. Kept first: field-specific rules below must not shadow\n // this with an unrelated rewrite when the named field happens to match their pattern.\n {\n matchApiError: /must be present in SELECT clause:\\s*'([\\w.]+)'/i,\n fix: (ctx) => {\n const field = ctx.apiErrorMessage?.match(/must be present in SELECT clause:\\s*'([\\w.]+)'/i)?.[1];\n if (!field) {\n return {\n action: \"add_required_field\",\n explanation: \"Add the field named in the error to the SELECT clause and retry\",\n };\n }\n const corrected = ctx.originalQuery.replace(/SELECT\\s+/i, `SELECT ${field}, `);\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: `${field} is used in WHERE but this resource requires it in SELECT too — added it`,\n };\n },\n },\n\n // 0b. OPERATOR_FIELD_MISMATCH — the field refuses this operator, and Google names the\n // ones it takes. Kept ahead of the field-specific rules for the same reason as rule 0.\n // A repeated field (final_urls, labels, …) takes only CONTAINS ALL/ANY/NONE, so it has\n // no null test at all: read it unfiltered and filter the rows instead.\n {\n matchApiError: /OPERATOR_FIELD_MISMATCH|is not a valid operator to use with/i,\n fix: (ctx) => {\n const message = ctx.apiErrorMessage ?? \"\";\n const field = message.match(/valid operator to use with '([\\w.]+)'/i)?.[1] ?? \"this field\";\n // Case-sensitive on purpose: Google writes operators uppercase, and with /i the capture would\n // also swallow any lowercase words just before the operator (\"the operator IS NULL\").\n const operator = message.match(/([A-Z=!<>][A-Z_ =!<>]*?) is not a valid operator/)?.[1]?.trim();\n const valid = message.match(/Valid operators: (.+?)\\.?(?:\"|$)/i)?.[1]?.trim();\n const refused = operator ? `${operator} cannot be used on ${field}` : `${field} does not take that operator`;\n const allowed = valid ? `; its valid operators are ${valid}` : \"\";\n const repeated = valid?.split(\",\").every((op) => /^CONTAINS (ALL|ANY|NONE)$/i.test(op.trim())) ?? false;\n const guidance = repeated\n ? `. ${listFieldGuidance(field)}`\n : \". Rewrite that condition with one of them, or drop it and filter the rows\";\n return {\n action: \"change_operator\",\n explanation: `${refused}${allowed}${guidance}`,\n };\n },\n },\n\n // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)\n {\n matchQuery: /(?<![\\w.])keyword\\.text\\b/,\n matchApiError: /keyword\\.text/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/(?<![\\w.])keyword\\.text\\b/g, \"ad_group_criterion.keyword.text\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Use ad_group_criterion.keyword.text — keywords are accessed through the criterion resource\",\n };\n },\n },\n\n // 2. bare keyword.match_type → ad_group_criterion.keyword.match_type (leaves <resource>.keyword.match_type untouched)\n {\n matchQuery: /(?<![\\w.])keyword\\.match_type\\b/,\n matchApiError: /keyword\\.match_type/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(\n /(?<![\\w.])keyword\\.match_type\\b/g,\n \"ad_group_criterion.keyword.match_type\",\n );\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Use ad_group_criterion.keyword.match_type for keyword match type\",\n };\n },\n },\n\n // 3. campaign_budget.* queried FROM campaign\n {\n matchQuery: /campaign_budget\\.\\w+.*FROM\\s+campaign\\b/i,\n matchApiError: /campaign_budget.*not.*valid.*campaign|cannot.*select.*campaign_budget/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/FROM\\s+campaign\\b/i, \"FROM campaign_budget\");\n return {\n action: \"use_different_resource\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"campaign_budget fields must be queried FROM campaign_budget, not FROM campaign\",\n };\n },\n },\n\n // 4. CONTAINS → LIKE. Keyed on the query alone: Google's error envelope (\"Request contains an\n // invalid argument … Valid operators: CONTAINS ANY\") matches any loose error pattern, and this\n // advice only makes sense when the query actually wrote CONTAINS(…).\n {\n matchQuery: /CONTAINS\\s*\\(/i,\n fix: (ctx) => {\n const match = ctx.originalQuery.match(/CONTAINS\\s*\\(\\s*([^,]+),\\s*'([^']+)'\\s*\\)/i);\n if (match) {\n const field = match[1]?.trim();\n const value = match[2] ?? \"\";\n const corrected = ctx.originalQuery.replace(\n /CONTAINS\\s*\\(\\s*[^,]+,\\s*'[^']+'\\s*\\)/i,\n `${field} LIKE '%${value}%'`,\n );\n return {\n action: \"change_operator\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"GAQL uses LIKE '%value%' for substring matching, not CONTAINS()\",\n };\n }\n return {\n action: \"change_operator\",\n explanation: \"Replace CONTAINS(field, 'value') with field LIKE '%value%'\",\n };\n },\n },\n\n // 5. ad_group_criterion query without negative filter — auto-add the field\n {\n matchQuery: /FROM\\s+ad_group_criterion\\b(?!.*ad_group_criterion\\.negative)/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/SELECT\\s+/i, \"SELECT ad_group_criterion.negative, \");\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation:\n \"Added ad_group_criterion.negative to distinguish positive (targeting) from negative (blocking) keywords. Filter with WHERE ad_group_criterion.negative = FALSE for positives only.\",\n };\n },\n },\n\n // 6. Missing campaign.id in ad_group queries\n {\n matchQuery: /FROM\\s+ad_group\\b/i,\n matchApiError: /campaign\\.id.*required|must.*include.*campaign\\.id/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/SELECT\\s+/i, \"SELECT campaign.id, \");\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Ad group queries require campaign.id in SELECT for parent context\",\n };\n },\n },\n\n // 6. Missing campaign.id in keyword_view queries\n {\n matchQuery: /FROM\\s+keyword_view\\b/i,\n matchApiError: /campaign\\.id.*required.*keyword/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/SELECT\\s+/i, \"SELECT campaign.id, \");\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Keyword view queries require campaign.id in SELECT\",\n };\n },\n },\n\n // 7. shopping_performance_view.product_* → segments.product_*\n {\n matchQuery: /shopping_performance_view\\.product_\\w+/,\n matchApiError: /shopping_performance_view\\.product/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/shopping_performance_view\\.product_(\\w+)/g, \"segments.product_$1\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Product fields are segments (segments.product_*), not fields on shopping_performance_view\",\n };\n },\n },\n\n // 8. Open-ended date range\n {\n matchQuery: /segments\\.date\\s*>=\\s*'(\\d{4}-\\d{2}-\\d{2})'/i,\n matchApiError: /date.*range.*must.*finite|open.*ended/i,\n fix: (ctx) => {\n const match = ctx.originalQuery.match(/segments\\.date\\s*>=\\s*'(\\d{4}-\\d{2}-\\d{2})'/i);\n const startDate = match?.[1] ?? \"2024-01-01\";\n const today = new Date().toISOString().slice(0, 10);\n const corrected = ctx.originalQuery.replace(\n /segments\\.date\\s*>=\\s*'(\\d{4}-\\d{2}-\\d{2})'/i,\n `segments.date BETWEEN '${startDate}' AND '${today}'`,\n );\n return {\n action: \"narrow_date_range\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Use BETWEEN with explicit end date — open-ended ranges are not supported\",\n };\n },\n },\n\n // 10a. Customer not found (wrong customer ID)\n {\n matchApiError: /CUSTOMER_NOT_FOUND|not.*found.*customer/i,\n fix: () => ({\n action: \"reject\",\n explanation:\n \"Customer ID not found. Run 'baker ads google accounts' to list valid customer IDs, then retry with a valid --customer-id.\",\n }),\n },\n\n // 10b. Not accessible / login-customer-id required\n {\n matchApiError: /not.*accessible|login.customer.id/i,\n fix: (ctx) => ({\n action: \"reject\",\n correctedCommand: buildCommand(ctx.originalQuery, ctx),\n explanation:\n \"Account not accessible — may need a manager (MCC) login-customer-id. Run 'baker ads google accounts' to refresh the accounts cache, then retry.\",\n }),\n },\n\n // 11. Campaign type by name matching\n {\n matchQuery: /campaign\\.name\\s*LIKE\\s*'%\\s*(shopping|pmax|search|display|video)\\s*%'/i,\n fix: (ctx) => {\n const match = ctx.originalQuery.match(/campaign\\.name\\s*LIKE\\s*'%\\s*(shopping|pmax|search|display|video)\\s*%'/i);\n const typeMap: Record<string, string> = {\n shopping: \"SHOPPING\",\n pmax: \"PERFORMANCE_MAX\",\n search: \"SEARCH\",\n display: \"DISPLAY\",\n video: \"VIDEO\",\n };\n const channelType = typeMap[match?.[1]?.toLowerCase() ?? \"\"] ?? \"SEARCH\";\n const corrected = ctx.originalQuery.replace(\n /campaign\\.name\\s*LIKE\\s*'%[^']*%'/i,\n `campaign.advertising_channel_type = '${channelType}'`,\n );\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Filter by campaign.advertising_channel_type enum, not by name pattern\",\n };\n },\n },\n\n // 12. Incompatible fields\n {\n matchApiError: /incompatible|mutually.*exclusive|cannot.*select.*together/i,\n fix: () => ({\n action: \"split_query\",\n explanation: \"These fields cannot be in the same query — split into separate queries and join client-side\",\n }),\n },\n\n // 13. campaign.status = 'ACTIVE' → 'ENABLED'\n {\n matchQuery: /campaign\\.status\\s*=\\s*'ACTIVE'/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/campaign\\.status\\s*=\\s*'ACTIVE'/gi, \"campaign.status = 'ENABLED'\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Campaign status uses ENABLED, not ACTIVE\",\n };\n },\n },\n\n // 14. ad_group.status = 'ACTIVE' → 'ENABLED'\n {\n matchQuery: /ad_group\\.status\\s*=\\s*'ACTIVE'/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/ad_group\\.status\\s*=\\s*'ACTIVE'/gi, \"ad_group.status = 'ENABLED'\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Ad group status uses ENABLED, not ACTIVE\",\n };\n },\n },\n\n // 15. ad.final_urls → ad_group_ad.ad.final_urls\n {\n matchQuery: /\\bad\\.final_urls\\b(?!.*ad_group_ad)/,\n matchApiError: /ad\\.final_urls/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/\\bad\\.final_urls\\b/g, \"ad_group_ad.ad.final_urls\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Use the full path: ad_group_ad.ad.final_urls\",\n };\n },\n },\n\n // 15b. asset.<type>_asset.final_urls → asset.final_urls (final URLs live on the asset\n // itself; the typed sub-message only carries type-specific fields like link_text)\n {\n matchQuery: /asset\\.\\w+_asset\\.final_(mobile_)?urls\\b/,\n matchApiError: /asset\\.\\w+_asset\\.final_(mobile_)?urls/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/asset\\.\\w+_asset\\.final_(mobile_)?urls\\b/g, \"asset.final_$1urls\");\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"Final URLs are on the asset itself — use asset.final_urls, not asset.<type>_asset.final_urls\",\n };\n },\n },\n\n // 16. Rate limit / quota\n {\n matchApiError: /RESOURCE_EXHAUSTED|quota.*exceeded|rate.*limit/i,\n fix: () => ({\n action: \"wait_and_retry\",\n explanation: \"API quota exceeded — wait 30 seconds before retrying\",\n }),\n },\n\n // 17. WHERE on metrics\n {\n matchApiError: /cannot.*filter.*metric|WHERE.*metrics|prohibited.*where/i,\n fix: () => ({\n action: \"reject\",\n explanation:\n \"Cannot filter on metrics in WHERE clause — remove the metrics filter and filter results client-side\",\n }),\n },\n\n // 18. ORDER BY field not in SELECT\n {\n matchApiError: /order.*by.*field.*not.*selected|must.*select.*field.*order/i,\n fix: (ctx) => {\n const orderMatch = ctx.originalQuery.match(/ORDER\\s+BY\\s+([\\w.]+)/i);\n const field = orderMatch?.[1] ?? \"field\";\n const corrected = ctx.originalQuery.replace(/SELECT\\s+/i, `SELECT ${field}, `);\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: `Add ${field} to SELECT — ORDER BY fields must be selected`,\n };\n },\n },\n\n // The change-history rules must stay ahead of rule 19: its `status.*filter`\n // pattern also matches Google's change_status errors (\"the change_status request\n // is missing filters on ...\"), and the first matching rule wins.\n // 18a. change_event past its 30-day reach → change_status, which reaches 90 days.\n // These three rules match on the error code only, never on the resource in the query.\n // A `matchQuery` here would also fire in the query-pattern pass — the fallback used\n // when no rule recognised the error — and answer every unrelated failure on a history\n // query with \"switch resources\", which is never the real fix. The backend forwards the\n // code as JSON (`\"changeEventError\":\"START_DATE_TOO_OLD\"`), so match that shape as well\n // as the dotted enum spelling.\n {\n matchApiError: /changeEventError\\W{0,4}START_DATE_TOO_OLD/i,\n fix: (ctx) => {\n const limit = ctx.originalQuery.match(/LIMIT\\s+(\\d+)/i)?.[1] ?? \"50\";\n const requested = ctx.originalQuery.match(/change_date_time\\s*>=\\s*'([^']+)'/i)?.[1]?.slice(0, 10);\n // 89, not 90: the window includes today, so counting back a full 90 days would put\n // the start one day outside what change_status accepts.\n const oldestStart = new Date(Date.now() - 89 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);\n const start = requested && requested > oldestStart ? requested : oldestStart;\n const corrected = `SELECT change_status.last_change_date_time, change_status.resource_type, change_status.resource_status, change_status.resource_name, change_status.campaign, change_status.ad_group FROM change_status WHERE change_status.last_change_date_time >= '${start}' ORDER BY change_status.last_change_date_time DESC LIMIT ${limit}`;\n return {\n action: \"use_different_resource\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: `change_event only reaches back 30 days. change_status reaches 90 (from ${start}) but reports only which resources changed and whether they were added, changed or removed — no old/new values and no user email. Read a changed resource by its resource_name to see its current values.`,\n };\n },\n },\n\n // 18b. Past 90 days no history resource reaches — infer the change from spend instead.\n {\n matchApiError: /changeStatusError\\W{0,4}START_DATE_TOO_OLD/i,\n fix: () => ({\n action: \"use_different_resource\",\n explanation: `Google's account history reaches back 90 days at most, and this query asks for more. Do not abort — infer the change from performance: ${monthlySpendTrendQuery()} shows which campaigns started or stopped spending and when, though not who changed them. Anything older with attribution exists only in the Google Ads web UI, which has no API.`,\n }),\n },\n\n // 18c. change_status rejects a date range it cannot bound. Google raises this both for\n // a half-open range and for no date filter at all, so supply whichever end is missing.\n {\n matchApiError: /CHANGE_DATE_RANGE_INFINITE/i,\n fix: (ctx) => {\n const today = new Date().toISOString().slice(0, 10);\n const oldestStart = new Date(Date.now() - 89 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);\n const hasLowerBound = /last_change_date_time\\s*>=?/i.test(ctx.originalQuery);\n const hasUpperBound = /last_change_date_time\\s*<=?/i.test(ctx.originalQuery);\n const bounds = [\n ...(hasLowerBound ? [] : [`change_status.last_change_date_time >= '${oldestStart}'`]),\n ...(hasUpperBound ? [] : [`change_status.last_change_date_time <= '${today}'`]),\n ];\n const hasWhere = /\\bWHERE\\b/i.test(ctx.originalQuery);\n const clause = hasWhere ? ` AND ${bounds.join(\" AND \")}` : ` WHERE ${bounds.join(\" AND \")}`;\n const corrected = /\\s+(ORDER\\s+BY|LIMIT)\\b/i.test(ctx.originalQuery)\n ? ctx.originalQuery.replace(/(\\s+)(ORDER\\s+BY|LIMIT)\\b/i, `${clause}$1$2`)\n : ctx.originalQuery.trimEnd() + clause;\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation:\n \"change_status needs a date range bounded at both ends — added the missing bound(s) on last_change_date_time.\",\n };\n },\n },\n\n // 19. Results include REMOVED entities\n {\n matchApiError: /REMOVED.*entities|status.*filter/i,\n fix: (ctx) => {\n const resourceMatch = ctx.originalQuery.match(/FROM\\s+(\\w+)/i);\n const resource = resourceMatch?.[1] ?? \"campaign\";\n const hasWhere = /WHERE/i.test(ctx.originalQuery);\n const suffix = hasWhere ? ` AND ${resource}.status != 'REMOVED'` : ` WHERE ${resource}.status != 'REMOVED'`;\n const limitMatch = ctx.originalQuery.match(/(\\s+LIMIT\\s+\\d+)/i);\n let corrected: string;\n if (limitMatch) {\n corrected = ctx.originalQuery.replace(/(\\s+LIMIT\\s+\\d+)/i, `${suffix}$1`);\n } else {\n corrected = ctx.originalQuery.trimEnd() + suffix;\n }\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: `Add status filter to exclude REMOVED ${resource}s`,\n };\n },\n },\n\n // 20. change_event without date constraint\n {\n matchQuery: /FROM\\s+change_event\\b(?!.*change_date_time)/i,\n matchApiError: /change_event.*date|date.*required.*change/i,\n fix: (ctx) => {\n const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);\n const hasWhere = /WHERE/i.test(ctx.originalQuery);\n const suffix = hasWhere\n ? ` AND change_event.change_date_time >= '${sevenDaysAgo}'`\n : ` WHERE change_event.change_date_time >= '${sevenDaysAgo}'`;\n const corrected = ctx.originalQuery.trimEnd() + suffix;\n return {\n action: \"add_required_field\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"change_event queries require a date constraint on change_date_time\",\n };\n },\n },\n\n // 21. resource_name = 'customers/X/Y/Z' → resource.id = Z\n {\n matchQuery: /(\\w+)\\.resource_name\\s*=\\s*'customers\\/\\d+\\/\\w+\\/(\\d+)'/i,\n fix: (ctx) => {\n const match = ctx.originalQuery.match(/(\\w+)\\.resource_name\\s*=\\s*'customers\\/\\d+\\/\\w+\\/(\\d+)'/i);\n const resource = match?.[1] ?? \"campaign\";\n const id = match?.[2] ?? \"ID\";\n const corrected = ctx.originalQuery.replace(\n /(\\w+)\\.resource_name\\s*=\\s*'customers\\/\\d+\\/\\w+\\/(\\d+)'/i,\n `${resource}.id = ${id}`,\n );\n return {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: `Simpler: use ${resource}.id = ${id} instead of resource_name path`,\n };\n },\n },\n\n // 22. segments.click_type incompatibility\n {\n matchApiError: /click_type.*incompatible|cannot.*click_type/i,\n fix: (ctx) => {\n const corrected = ctx.originalQuery.replace(/,?\\s*segments\\.click_type/g, \"\");\n return {\n action: \"split_query\",\n correctedCommand: buildCommand(corrected, ctx),\n explanation: \"segments.click_type is incompatible with other segments — remove it or query separately\",\n };\n },\n },\n\n // 22b. A field this API version does not have (removed, renamed, or misspelled). The rules above\n // rewrite the known renames; anything left is named back so the agent drops or replaces it —\n // without this, \"invalid argument … field\" fell through to the bare-resource rule below.\n {\n matchApiError: /UNRECOGNIZED_FIELD|Unrecognized field in the query/i,\n fix: (ctx) => {\n const field = ctx.apiErrorMessage?.match(/Unrecognized field in the query:\\s*\\\\?'([\\w.]+)/i)?.[1];\n return {\n action: \"retry_with_modified_query\",\n explanation: field\n ? `${field} does not exist in this Google Ads API version (removed, renamed or misspelled). Drop it, or find its replacement in Google's field reference for that resource.`\n : \"A field in this query does not exist in this Google Ads API version. Drop it, or find its replacement in Google's field reference.\",\n };\n },\n },\n\n // 22c. A field that exists but cannot be SELECTed (only filtered on, or read through another resource).\n {\n matchApiError: /PROHIBITED_FIELD_IN_SELECT_CLAUSE|may not be used in SELECT clause/i,\n fix: (ctx) => {\n const field = ctx.apiErrorMessage?.match(/may not be used in SELECT clause:\\s*\\\\?'([\\w.]+)/i)?.[1];\n return {\n action: \"retry_with_modified_query\",\n explanation: `${field ?? \"A field in this query\"} cannot be selected — Google allows it only in WHERE, or not at all on this resource. Drop it from SELECT.`,\n };\n },\n },\n\n // 23. Bare resource in SELECT\n {\n matchApiError: /cannot.*select.*bare.*resource|invalid.*field/i,\n fix: () => ({\n action: \"retry_with_modified_query\",\n explanation: \"Cannot SELECT a bare resource name — use resource.id, resource.name, or resource.resource_name\",\n }),\n },\n\n // 24. Authentication error\n {\n matchApiError: /unauthenticated|authentication.*required|invalid.*credentials/i,\n fix: () => ({\n action: \"authenticate\",\n explanation: \"Google Ads authentication failed — reconnect Google Ads in dashboard settings\",\n }),\n },\n\n // 25. Timeout / deadline exceeded\n {\n matchApiError: /timeout|deadline.*exceeded|DEADLINE_EXCEEDED/i,\n fix: (ctx) => ({\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(ctx.originalQuery, ctx),\n explanation: \"Query timed out — narrow the date range, add more WHERE filters, or reduce LIMIT\",\n }),\n },\n];\n","import { connectionFixExplanation, isNotConnectedError } from \"../../../error-handler.ts\";\nimport type { AdsErrorEnvelope, CorrectionContext } from \"../types.ts\";\nimport { CORRECTION_RULES } from \"./correction-table.ts\";\n\nfunction mapErrorCode(message: string): string {\n if (/must be present in SELECT clause/i.test(message)) return \"MISSING_SELECT_FIELD\";\n if (/field.*not.*found|unrecognized.*field|not.*valid.*field/i.test(message)) return \"FIELD_NOT_FOUND\";\n if (/not.*valid.*resource|cannot.*select.*from/i.test(message)) return \"WRONG_RESOURCE\";\n if (/operator|CONTAINS|LIKE/i.test(message)) return \"INVALID_OPERATOR\";\n if (/CUSTOMER_NOT_FOUND/i.test(message)) return \"CUSTOMER_NOT_FOUND\";\n if (/login.customer.id|not.*accessible/i.test(message)) return \"MISSING_MANAGER_ID\";\n if (/incompatible|mutually.*exclusive|PROHIBITED_SEGMENT_WITH_METRIC/i.test(message)) return \"INCOMPATIBLE_FIELDS\";\n if (/unauthenticated|authentication/i.test(message)) return \"AUTH_ERROR\";\n if (/permission/i.test(message)) return \"PERMISSION_DENIED\";\n if (/RESOURCE_EXHAUSTED|quota|rate.*limit/i.test(message)) return \"QUOTA_EXCEEDED\";\n if (/timeout|DEADLINE_EXCEEDED/i.test(message)) return \"TIMEOUT\";\n return \"API_ERROR\";\n}\n\nfunction isRetryable(code: string): boolean {\n return code === \"QUOTA_EXCEEDED\" || code === \"TIMEOUT\";\n}\n\nfunction getRetryDelay(code: string): number | undefined {\n if (code === \"QUOTA_EXCEEDED\") return 30000;\n if (code === \"TIMEOUT\") return 5000;\n return undefined;\n}\n\n/**\n * `apiCode` is the client's HTTP mapping (`ApiError.code`). Without it a missing\n * connection is indistinguishable from a GAQL error, and the agent gets told to\n * fix its query when the real answer is \"connect the account\".\n */\nexport function parseApiError(\n errorMessage: string,\n originalQuery: string,\n customerId: string,\n apiCode?: string,\n): AdsErrorEnvelope {\n const ctx: CorrectionContext = {\n originalQuery,\n customerId,\n apiErrorMessage: errorMessage,\n };\n\n // Checked ahead of the correction rules: no query rewrite can fix a missing connection.\n if (apiCode && isNotConnectedError(apiCode, errorMessage)) {\n return {\n ok: false,\n error: {\n code: \"NO_GOOGLE_ADS_CONNECTION\",\n message: errorMessage,\n fix: { action: \"request_connection\", explanation: connectionFixExplanation(\"google_ads\") },\n retryable: false,\n },\n };\n }\n\n // Try API error patterns first\n for (const rule of CORRECTION_RULES) {\n if (rule.matchApiError?.test(errorMessage)) {\n const fix = rule.fix(ctx);\n const code = mapErrorCode(errorMessage);\n return {\n ok: false,\n error: {\n code,\n message: errorMessage,\n fix,\n retryable: isRetryable(code),\n retryAfterMs: getRetryDelay(code),\n },\n };\n }\n }\n\n // Try query patterns (API might reject what preflight missed)\n for (const rule of CORRECTION_RULES) {\n if (rule.matchQuery?.test(originalQuery)) {\n const fix = rule.fix(ctx);\n const code = mapErrorCode(errorMessage);\n return {\n ok: false,\n error: {\n code,\n message: errorMessage,\n fix,\n retryable: isRetryable(code),\n retryAfterMs: getRetryDelay(code),\n },\n };\n }\n }\n\n // Fallback: unknown error\n const code = mapErrorCode(errorMessage);\n return {\n ok: false,\n error: {\n code,\n message: errorMessage,\n fix: {\n action: \"reject\",\n explanation: \"Unrecognized error — verify query syntax against Google Ads GAQL reference\",\n },\n retryable: isRetryable(code),\n retryAfterMs: getRetryDelay(code),\n },\n };\n}\n"],"mappings":"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","names":["code"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/ads/field-descriptions.ts"],"sourcesContent":["const FIELD_DESCRIPTIONS: Record<string, string> = {\n // Customer\n \"customer.id\": \"Google Ads account ID\",\n \"customer.descriptive_name\": \"Account display name\",\n \"customer.currency_code\": \"Account currency (e.g. USD, EUR)\",\n \"customer.time_zone\": \"Account timezone (e.g. America/New_York)\",\n\n // Campaign\n \"campaign.id\": \"Unique campaign identifier\",\n \"campaign.name\": \"Campaign display name\",\n \"campaign.status\": \"ENABLED, PAUSED, or REMOVED\",\n \"campaign.advertising_channel_type\": \"Campaign type: SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX, DEMAND_GEN\",\n \"campaign.bidding_strategy_type\": \"Bidding strategy (TARGET_CPA, TARGET_ROAS, MAXIMIZE_CONVERSIONS, etc.)\",\n \"campaign.primary_status\":\n \"Whether the campaign can serve: ELIGIBLE, PAUSED, REMOVED, ENDED, PENDING, MISCONFIGURED, LIMITED, LEARNING, NOT_ELIGIBLE. Unlike the keyword equivalent, LIMITED is a real value here — but Google publishes no reason-to-status mapping, so decide 'Limited by budget' from primary_status_reasons, not from this field.\",\n \"campaign.primary_status_reasons\":\n \"Why the campaign is limited or not serving (repeated field — an array). BUDGET_CONSTRAINED is 'Limited by budget'; BUDGET_MISCONFIGURED is a broken budget setting, not a cap. Since 17 Aug 2026 a budget-limited campaign on a target-based strategy delivers toward its target instead of toward its recent actual cost, so BUDGET_CONSTRAINED is the flag that says a target above actual now costs money. BIDDING_STRATEGY_LEARNING means the bidding system is still adjusting to a change.\",\n \"campaign.target_cpa.target_cpa_micros\": \"Target CPA in micros on a standalone Target CPA strategy\",\n \"campaign.maximize_conversions.target_cpa_micros\":\n \"Target CPA in micros when the target is held inside a Maximize Conversions strategy — the shape Google uses on Search\",\n \"campaign.target_roas.target_roas\": \"Target ROAS as a ratio (2.5 = 250%) on a standalone Target ROAS strategy\",\n \"campaign.maximize_conversion_value.target_roas\":\n \"Target ROAS as a ratio when held inside a Maximize Conversion Value strategy\",\n \"campaign.target_cpc.target_cpc_micros\":\n \"Target CPC in micros on a Demand Gen Target CPC strategy. Governed by the same August 2026 change as Target CPA/ROAS: on a budget-limited campaign it is what delivery moves toward, not a ceiling.\",\n \"ad_group.target_cpc_micros\": \"Ad-group override of the campaign's Demand Gen target CPC, in micros\",\n \"campaign.start_date\": \"Campaign start date (YYYY-MM-DD)\",\n \"campaign.end_date\": \"Campaign end date (YYYY-MM-DD), blank if ongoing\",\n\n // Campaign budget\n \"campaign_budget.amount_micros\": \"Daily budget in micros (÷ 1,000,000 for actual currency)\",\n\n // Ad group\n \"ad_group.id\": \"Unique ad group identifier\",\n \"ad_group.name\": \"Ad group display name\",\n \"ad_group.status\": \"ENABLED, PAUSED, or REMOVED\",\n \"ad_group.type\": \"Ad group type (SEARCH_STANDARD, DISPLAY_STANDARD, etc.)\",\n\n // Ad group ad\n \"ad_group_ad.ad.id\": \"Unique ad identifier\",\n \"ad_group_ad.ad.final_urls\": \"Landing page URLs (array)\",\n \"ad_group_ad.ad.type\": \"Ad format (RESPONSIVE_SEARCH_AD, etc.)\",\n \"ad_group_ad.status\": \"ENABLED, PAUSED, or REMOVED\",\n \"ad_group_ad.ad.responsive_search_ad.headlines\": \"Ad headline variations (array)\",\n \"ad_group_ad.ad.responsive_search_ad.descriptions\": \"Ad description variations (array)\",\n\n // Keywords\n \"ad_group_criterion.criterion_id\":\n \"Keyword criterion id — derived from text + match type, so every copy of the same keyword across ad groups shares it. Pair with ad_group.id to identify one copy (writes take customers/<cid>/adGroupCriteria/<adGroupId>~<criterionId>)\",\n \"ad_group_criterion.keyword.text\": \"The keyword text\",\n \"ad_group_criterion.keyword.match_type\": \"EXACT, PHRASE, or BROAD\",\n \"ad_group_criterion.status\": \"ENABLED, PAUSED, or REMOVED\",\n \"ad_group_criterion.quality_info.quality_score\": \"Quality score 1-10 (higher is better)\",\n \"ad_group_criterion.quality_info.creative_quality_score\":\n \"Ad relevance bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE\",\n \"ad_group_criterion.quality_info.post_click_quality_score\":\n \"Landing page experience bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE\",\n \"ad_group_criterion.quality_info.search_predicted_ctr\":\n \"Expected click-through rate bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE\",\n \"ad_group_criterion.primary_status\":\n \"Whether the keyword can serve: ELIGIBLE, PAUSED, REMOVED, PENDING, NOT_ELIGIBLE. There is no LIMITED value — Google Ads' 'Eligible (Limited)' is ELIGIBLE plus a limiting entry in primary_status_reasons, so read both.\",\n \"ad_group_criterion.primary_status_reasons\":\n \"Why the keyword is in that status (array). AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID and AD_GROUP_CRITERION_LOW_QUALITY are the two that render as '(Limited)'; the CAMPAIGN_*/AD_GROUP_* entries just restate a paused or removed parent.\",\n \"ad_group_criterion.system_serving_status\": \"ELIGIBLE or RARELY_SERVED (too few searches to show)\",\n \"ad_group_criterion.approval_status\": \"Ad-approval state: APPROVED, DISAPPROVED, PENDING_REVIEW, UNDER_REVIEW\",\n \"ad_group_criterion.disapproval_reasons\": \"Policy reasons the keyword was disapproved (array)\",\n \"ad_group_criterion.cpc_bid_micros\": \"Max CPC bid set on this keyword, in micros (÷ 1,000,000)\",\n \"ad_group_criterion.effective_cpc_bid_micros\":\n \"Max CPC actually in force, in micros (÷ 1,000,000) — the keyword's own bid, or the ad group's when the keyword has none\",\n \"ad_group_criterion.effective_cpc_bid_source\":\n \"Where the effective max CPC comes from: ADGROUP, CRITERION, or CAMPAIGN_BIDDING_STRATEGY\",\n \"ad_group_criterion.position_estimates.first_page_cpc_micros\":\n \"Bid estimated to reach the first page of results, in micros (÷ 1,000,000). The number in Google Ads' 'Below first page bid (€3.63)' label — compare it against effective_cpc_bid_micros.\",\n \"ad_group_criterion.position_estimates.top_of_page_cpc_micros\":\n \"Bid estimated to reach the top of the first page, in micros (÷ 1,000,000)\",\n \"ad_group_criterion.position_estimates.first_position_cpc_micros\":\n \"Bid estimated to reach the first ad position, in micros (÷ 1,000,000)\",\n\n // Search terms\n \"search_term_view.search_term\": \"Actual user search query that triggered the ad\",\n \"search_term_view.status\": \"Whether the search term is added, excluded, or none\",\n\n // Metrics\n \"metrics.impressions\": \"Times ads were shown (integer)\",\n \"metrics.clicks\": \"Total clicks (integer)\",\n \"metrics.cost_micros\":\n \"Total cost in micros (÷ 1,000,000 for actual currency). Use 'currency' command for account currency.\",\n \"metrics.cost\": \"Total cost in actual currency (already converted from micros)\",\n \"metrics.conversions\": \"Conversion count (can be fractional from attribution models)\",\n \"metrics.conversions_value\": \"Revenue from conversions in actual currency\",\n \"metrics.all_conversions\": \"All conversions including view-through\",\n \"metrics.all_conversions_value\": \"Total attributed revenue including view-through\",\n \"metrics.ctr\": \"Click-through rate as decimal (0.05 = 5%)\",\n \"metrics.average_cpc\": \"Average cost per click in micros (÷ 1,000,000)\",\n \"metrics.average_cpm\": \"Average cost per 1000 impressions in micros (÷ 1,000,000)\",\n \"metrics.search_impression_share\": \"Impression share as decimal (0.0–1.0)\",\n \"metrics.search_budget_lost_impression_share\":\n \"Share of impressions lost because the budget ran out, as a decimal (0.0–1.0). The corroborating signal for a 'Limited by budget' campaign — a high value here alongside a target above actual cost per conversion is the combination that now raises spend.\",\n \"metrics.cost_per_conversion\":\n \"Actual cost per conversion in micros (÷ 1,000,000) — compare against the campaign's target\",\n \"metrics.interaction_rate\": \"Interaction rate as decimal\",\n \"metrics.video_views\": \"Total video views (integer)\",\n \"metrics.video_view_rate\": \"Video view rate as decimal\",\n\n // Segments\n \"segments.date\": \"Date (YYYY-MM-DD format)\",\n \"segments.device\": \"DESKTOP, MOBILE, TABLET, or OTHER\",\n \"segments.day_of_week\": \"Day of week (MONDAY, TUESDAY, etc.)\",\n \"segments.ad_network_type\": \"Network: SEARCH, SEARCH_PARTNERS, CONTENT, YOUTUBE_SEARCH, etc.\",\n \"segments.product_title\": \"Shopping product title\",\n \"segments.product_item_id\": \"Shopping product ID\",\n \"segments.product_brand\": \"Shopping product brand\",\n \"segments.product_type_l1\": \"Shopping product type (level 1)\",\n\n // Assets\n \"asset.type\": \"Asset type: IMAGE, YOUTUBE_VIDEO, TEXT, LEAD_FORM, etc.\",\n \"asset.text_asset.text\": \"Text asset content\",\n \"asset.image_asset.full_size.url\": \"Full-size image URL\",\n \"asset.youtube_video_asset.youtube_video_id\": \"YouTube video ID\",\n \"asset.youtube_video_asset.youtube_video_title\": \"YouTube video title\",\n \"asset_group.name\": \"Asset group display name\",\n \"asset_group.status\": \"ENABLED, PAUSED, or REMOVED\",\n \"asset_group_asset.field_type\": \"Where the asset is used (HEADLINE, DESCRIPTION, LOGO, etc.)\",\n \"asset_group_asset.primary_status\":\n \"Whether the asset serves: ELIGIBLE, LIMITED, NOT_ELIGIBLE, PAUSED, REMOVED, PENDING\",\n \"asset_group_asset.primary_status_reasons\":\n \"Why the asset is not fully serving (policy, pending review, paused parent …)\",\n\n // Change events (field-level detail, last 30 days)\n \"change_event.change_date_time\": \"When the change occurred (ISO 8601)\",\n \"change_event.change_resource_type\": \"What was changed: CAMPAIGN, AD_GROUP, AD, etc.\",\n \"change_event.resource_change_operation\": \"CREATE, UPDATE, or REMOVE\",\n \"change_event.user_email\": \"Email of user who made the change\",\n\n // Change status (which resources changed, last 90 days — no values, no user)\n \"change_status.last_change_date_time\": \"When this resource last changed (ISO 8601)\",\n \"change_status.resource_type\": \"What changed: CAMPAIGN, AD_GROUP, AD_GROUP_AD, AD_GROUP_CRITERION, etc.\",\n \"change_status.resource_status\": \"ADDED, CHANGED, or REMOVED — not the old or new value\",\n \"change_status.resource_name\": \"Identifier of the changed resource; query it to read its current values\",\n \"change_status.campaign\": \"Resource name of the campaign the change belongs to\",\n \"change_status.ad_group\": \"Resource name of the ad group the change belongs to\",\n \"change_status.ad_group_ad\": \"Resource name of the changed ad\",\n \"change_status.ad_group_criterion\": \"Resource name of the changed keyword or criterion\",\n \"change_status.campaign_criterion\": \"Resource name of the changed campaign-level criterion\",\n\n // Landing pages\n \"landing_page_view.unexpanded_final_url\": \"Landing page URL\",\n\n // Geo\n \"geo_target_constant.name\": \"Geographic location name\",\n \"geo_target_constant.resource_name\": \"Geo target resource identifier\",\n};\n\nexport function getFieldDescriptions(fields: string[]): Record<string, string> {\n const result: Record<string, string> = {};\n for (const field of fields) {\n const desc = FIELD_DESCRIPTIONS[field];\n if (desc) {\n result[field] = desc;\n }\n }\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/ads/google/query.ts","../src/commands/ads/google/presets.ts","../src/commands/ads/google/preflight.ts","../src/commands/ads/google/conversion-mix.ts","../src/commands/ads/google/keyword-serving.ts","../src/commands/ads/google/money-units.ts"],"sourcesContent":["import { appendFileSync, existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { GOOGLE_ADS_QUERY_MAX_PAGE_SIZE } from \"@baker/api/ads-google\";\nimport { defineCommand } from \"citty\";\nimport { ApiError, apiPost } from \"../../../client.ts\";\nimport { registerSchema } from \"../../../schemas.ts\";\nimport { buildQueryCacheKey, cacheGet, cacheSet, getManagerIdForCustomer, getQueryTtl } from \"../cache.ts\";\nimport { getFieldDescriptions } from \"../field-descriptions.ts\";\nimport { resolveCustomerId, toCsvRow, writeAdsJson } from \"../output.ts\";\nimport { googlePresetSyncedHint } from \"../synced-first.ts\";\nimport type { AdsErrorEnvelope, AdsFileSummary, AdsSuccessEnvelope, AdsWarning } from \"../types.ts\";\nimport { conversionMixWarnings } from \"./conversion-mix.ts\";\nimport { parseApiError } from \"./error-parser.ts\";\nimport {\n collectKeywordLimits,\n type KeywordLimits,\n type KeywordServingOptions,\n keywordServingWarnings,\n renderKeywordLimits,\n} from \"./keyword-serving.ts\";\nimport { moneyUnitWarnings } from \"./money-units.ts\";\nimport { isSingleRowRead, validatePreflight } from \"./preflight.ts\";\nimport { expandPreset, getPreset, isInventoryPreset, PRESETS, SERVING_CHAINS } from \"./presets.ts\";\n\nregisterSchema({\n command: \"ads.google.query\",\n description:\n \"Run a GAQL (Google Ads Query Language) query, one page at a time. Start here: `baker ads google query --preset campaign-performance` (--list-presets for all). Each read returns one page of --limit rows and, when the account holds more, `pagination.cursor` — pass it back with --cursor for the next page, or use --all --out <file> to read every page. A LIMIT in your own GAQL caps the whole read.\",\n args: {\n query: { type: \"positional\", description: \"GAQL SELECT statement (or pass it with --gaql)\", required: false },\n gaql: {\n type: \"string\",\n description:\n 'GAQL SELECT statement as a flag — same as the positional, for when quoting a positional is awkward: --gaql \"SELECT campaign.name FROM campaign\"',\n required: false,\n },\n \"customer-id\": {\n type: \"string\",\n description: \"Google Ads customer ID (10 digits, no dashes). Falls back to BAKER_GOOGLE_ADS_CUSTOMER_ID env var.\",\n required: false,\n },\n preset: {\n type: \"string\",\n description: `Named query preset (--list-presets shows each one's GAQL and page size): ${PRESETS.map((p) => p.name).join(\", \")}. Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default — pass --include-paused to include paused ones.`,\n required: false,\n },\n \"date-range\": {\n type: \"string\",\n description:\n \"Date range for presets: a Google date literal (TODAY, YESTERDAY, LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH …) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'. Google has no LAST_90_DAYS — use BETWEEN for any other window.\",\n required: false,\n },\n \"include-paused\": {\n type: \"boolean\",\n description:\n \"Include paused entities in preset results. Off by default: presets return only actually-serving entities. Google Ads has no single serving flag — an entity serves only when its whole chain (campaign, ad group, ad) is ENABLED, so recommendations should target serving entities unless the user asks about paused ones.\",\n required: false,\n },\n limit: {\n type: \"number\",\n description: `Rows per page (default: the preset's page size, else 200; at most ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE}). A page size, not a cap: when more rows exist the read returns a cursor, and --all keeps paging.`,\n required: false,\n },\n \"list-presets\": { type: \"boolean\", description: \"List all available query presets\", required: false },\n cursor: {\n type: \"string\",\n description: \"Resume after a previous page: pass its pagination.cursor with the same query and --limit\",\n required: false,\n },\n all: {\n type: \"boolean\",\n description:\n \"Follow every page until the account has no more rows (use with --out for large reads). Needed for any read you will count or total. Only a LIMIT written in the GAQL itself can stop it early.\",\n required: false,\n },\n out: {\n type: \"string\",\n description: \"Write data to file (format from extension: .csv, .jsonl, .json)\",\n required: false,\n },\n append: { type: \"boolean\", description: \"Append to existing file (skip CSV headers)\", required: false },\n output: {\n type: \"string\",\n description: \"Output format: json|csv|jsonl|md\",\n required: false,\n default: \"json\",\n enum: [\"json\", \"csv\", \"jsonl\", \"md\"],\n },\n \"no-cache\": { type: \"boolean\", description: \"Skip cache, hit API directly\", required: false },\n full: {\n type: \"boolean\",\n description:\n \"Include the field description glossary in the JSON envelope. Off by default — rows already carry the data; only ask for this when you need a field's meaning explained.\",\n required: false,\n },\n },\n});\n\ninterface QueryApiResponse {\n rows: Array<Record<string, unknown>>;\n pageToken?: string;\n}\n\nfunction extractFields(rows: Array<Record<string, unknown>>): string[] {\n const keys = new Set<string>();\n foldObservedFields(rows, keys);\n return [...keys];\n}\n\n// The columns a table-shaped read renders: every SELECTed field, in SELECT order, whether or not any\n// row carried it. Deriving them from the rows is what dropped `campaign_conversion_goal.biddable` from\n// an md table: Google omits a false boolean, the first row was a non-biddable goal, and the column\n// that answered the question vanished with no warning. A submessage selected whole expands to the\n// leaves that came back, and a key nobody selected is kept at the end rather than lost.\nexport function resultColumns(gaql: string, rows: Array<Record<string, unknown>>): string[] {\n const observed = extractFields(rows);\n const columns: string[] = [];\n for (const field of parseSelectedFields(gaql)) {\n const leaves = observed.filter((key) => key.startsWith(`${field}.`));\n columns.push(...(leaves.length > 0 ? leaves : [field]));\n }\n for (const key of observed) columns.push(key);\n return [...new Set(columns)];\n}\n\nfunction isCsvPath(filePath: string): boolean {\n return filePath.split(\".\").pop()?.toLowerCase() === \"csv\";\n}\n\nfunction writeRowsToFile(\n filePath: string,\n rows: Array<Record<string, unknown>>,\n fields: string[],\n append: boolean,\n): void {\n if (isCsvPath(filePath)) {\n if (!append || !existsSync(filePath)) {\n writeFileSync(filePath, `${toCsvRow(fields)}\\n`, \"utf-8\");\n }\n const csvRows = rows.map((row) => toCsvRow(fields.map((f) => String(row[f] ?? \"\"))));\n appendFileSync(filePath, `${csvRows.join(\"\\n\")}\\n`, \"utf-8\");\n } else if (filePath.split(\".\").pop()?.toLowerCase() === \"jsonl\") {\n const lines = rows.map((row) => JSON.stringify(row));\n if (append && existsSync(filePath)) {\n appendFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n } else {\n writeFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n }\n } else if (append && existsSync(filePath)) {\n const existing = JSON.parse(readFileSync(filePath, \"utf-8\")) as unknown[];\n writeFileSync(filePath, JSON.stringify([...existing, ...rows], null, 2), \"utf-8\");\n } else {\n writeFileSync(filePath, JSON.stringify(rows, null, 2), \"utf-8\");\n }\n}\n\n// The row cap in force for the whole read, or undefined when nothing caps it. --limit is only the\n// size of a page — the read hands back a cursor when the account holds more — so the one cap left is\n// a LIMIT the statement itself carries. Needed to tell \"this is all there is\" from \"this is as much\n// as the statement asked for\".\n//\n// A statement that returns one row by construction has no cap in force: its LIMIT 1 is the shape of\n// the answer, not a page size, and one row filling it is the whole data set.\nexport function effectiveRowLimit(finalQuery: string): number | undefined {\n if (isSingleRowRead(finalQuery)) return undefined;\n const match = finalQuery.match(/\\bLIMIT\\s+(\\d+)/i);\n return match?.[1] ? Number(match[1]) : undefined;\n}\n\n// One notice per warning, shaped so it survives the format it travels in. The channel never names\n// a warning code: whatever the preflight or a row decoder produces flows through unchanged.\nexport function warningLines(warnings: AdsWarning[], format: string): string[] {\n return warnings.map((w) => {\n const message = w.message.replace(/\\s*\\n\\s*/g, \" \");\n if (format === \"jsonl\") return JSON.stringify({ _baker_warning: { code: w.code, message } });\n if (format === \"md\") return `> **${w.code}** — ${message}`;\n // csv and anything else: a leading # keeps a CSV parser from reading it as a header.\n return `# baker ${w.code}: ${message}`;\n });\n}\n\nexport interface ReadCompleteness {\n rowCount: number;\n /** The statement's own LIMIT — see `effectiveRowLimit`. Undefined when nothing caps the read. */\n effectiveLimit: number | undefined;\n /** The account handed back a next-page cursor that this run did not follow. */\n hasMore: boolean;\n}\n\n// A truncated read that looks complete is the whole failure this guard exists for: rows that stop\n// at a cap are indistinguishable from a data set that happens to be that size, and the agent reads\n// them as the universe. Two things can stop a read short — an unfollowed cursor, or the statement's\n// own LIMIT — and each has its own way on. A read that ran out of rows says nothing.\nexport function completenessWarnings(read: ReadCompleteness): AdsWarning[] {\n const cap = read.effectiveLimit;\n const filledTheCap = cap !== undefined && cap > 0 && read.rowCount >= cap;\n if (!read.hasMore && !filledTheCap) return [];\n const [cause, fix] = read.hasMore\n ? [\n \"More rows are waiting: the account returned a next-page cursor this run did not follow.\",\n \"Re-run with --all --out <file> to read every page, or pass --cursor <pagination.cursor> (same query, same --limit) for the next page.\",\n ]\n : [\n `The ${read.rowCount} rows returned exactly fill the LIMIT in the statement (${cap}).`,\n `Raise the LIMIT in the statement above ${cap}, or drop it — without a LIMIT, --all pages through every row.`,\n ];\n return [\n {\n code: \"TRUNCATED_RESULTS\",\n message: `INCOMPLETE READ — ${cause} These rows are a page, not the data set. Don't count, total or describe them as the whole picture. ${fix}`,\n },\n ];\n}\n\ninterface EnvelopeOptions {\n full: boolean;\n cached: boolean;\n nextCursor?: string;\n}\n\n// The field-description dictionary is a static glossary the agent almost never reads, so it is\n// opt-in via --full rather than a per-call token tax on every JSON envelope.\nexport function buildQueryEnvelope(\n rows: Array<Record<string, unknown>>,\n fieldDescs: Record<string, string>,\n warnings: AdsWarning[],\n options: EnvelopeOptions,\n): AdsSuccessEnvelope<Array<Record<string, unknown>>> {\n // Key order is load-bearing: a pretty-printed envelope of 500 rows read through `head` shows only\n // its first keys, so warnings and pagination go BEFORE data, and a one-line notice closes it for\n // whoever reads the tail.\n const notice = summariseWarnings(warnings);\n return {\n ok: true,\n ...(options.cached ? { cached: true as const } : {}),\n ...(warnings.length > 0 ? { warnings } : {}),\n ...(options.nextCursor ? { pagination: { hasMore: true, cursor: options.nextCursor } } : {}),\n data: rows,\n ...(options.full && Object.keys(fieldDescs).length > 0 ? { fields: fieldDescs } : {}),\n ...(notice ? { notice } : {}),\n };\n}\n\n// One line naming every warning code, so a reader who only sees the end of the output still knows\n// something was flagged. Code-agnostic: any warning the pipeline produces lands here.\nexport function summariseWarnings(warnings: AdsWarning[]): string | undefined {\n if (warnings.length === 0) return undefined;\n const codes = [...new Set(warnings.map((w) => w.code))].join(\", \");\n return `Read the \"warnings\" above before using these rows: ${codes}.`;\n}\n\nexport interface FileSummaryInput {\n file: string;\n rows: number;\n warnings: AdsWarning[];\n complete: boolean;\n}\n\n// The --out branch throws the rows away, so this envelope is the only thing the agent reads about\n// the pull. It carries every warning the read raised, not just the ones a row decoder produced.\nexport function buildFileSummary(input: FileSummaryInput): AdsFileSummary {\n const notice = summariseWarnings(input.warnings);\n return {\n ok: true,\n fields: {},\n file: input.file,\n rows: input.rows,\n complete: input.complete,\n ...(input.warnings.length > 0 ? { warnings: input.warnings } : {}),\n ...(notice ? { notice } : {}),\n };\n}\n\n// Repeating the notice at the tail costs two lines and buys the case that actually bit us: a long\n// CSV read whose output the agent inspects with `tail`. Short reads are visible whole, so once is enough.\nconst REPEAT_NOTICE_ABOVE_ROWS = 20;\n\nexport interface ReadContext {\n /** The statement that ran — its SELECT list, not the rows, decides a table's columns. */\n gaql: string;\n effectiveLimit: number | undefined;\n nextCursor?: string;\n}\n\n// stderr is the complementary channel, never the only one — the failing session redirected it to\n// /dev/null 13 times.\nfunction emitToStderr(notices: string[]): void {\n for (const line of notices) process.stderr.write(`${line}\\n`);\n}\n\nfunction writeLine(line: string): void {\n process.stdout.write(`${line}\\n`);\n}\n\ntype RowWriter = (rows: Array<Record<string, unknown>>, fields: string[], notices: string[]) => void;\n\n// The header stays the first line so a CSV reader still parses; the notice sits right under it,\n// where a `head` lands, and repeats at the tail, where a `tail` lands.\nconst writeCsvRows: RowWriter = (rows, fields, notices) => {\n writeLine(toCsvRow(fields));\n for (const line of notices) writeLine(line);\n for (const row of rows) writeLine(toCsvRow(fields.map((f) => String(row[f] ?? \"\"))));\n};\n\nconst writeJsonlRows: RowWriter = (rows, _fields, notices) => {\n for (const line of notices) writeLine(line);\n for (const row of rows) writeLine(JSON.stringify(row));\n};\n\nconst OMITTED = \"(omitted)\";\nconst OMITTED_FALSE = `false ${OMITTED}`;\nconst OMITTED_LEGEND = `> ${OMITTED} = Google left this field off the row, which is how it sends false, 0 and \"not set\": the field WAS read. A field with no column here was not read.`;\n\n// A blank cell reads as \"no data\", which is the one thing an omitted field is not. Google omits a\n// false boolean, so in a column that carries a boolean anywhere the omission is spelled out as false.\nfunction mdCell(row: Record<string, unknown>, field: string, booleanColumns: ReadonlySet<string>): string {\n if (!(field in row)) return booleanColumns.has(field) ? OMITTED_FALSE : OMITTED;\n return String(row[field] ?? \"\").replace(/\\|/g, \"\\\\|\");\n}\n\n// A blockquote inside a markdown table would break it, so the notice brackets the table.\nconst writeMdRows: RowWriter = (rows, fields, notices) => {\n const booleanColumns = new Set(fields.filter((f) => rows.some((row) => typeof row[f] === \"boolean\")));\n for (const line of notices) writeLine(line);\n writeLine(`| ${fields.join(\" | \")} |`);\n writeLine(`| ${fields.map(() => \"---\").join(\" | \")} |`);\n for (const row of rows) writeLine(`| ${fields.map((f) => mdCell(row, f, booleanColumns)).join(\" | \")} |`);\n if (rows.some((row) => fields.some((f) => !(f in row)))) writeLine(OMITTED_LEGEND);\n};\n\nconst ROW_WRITERS: Record<string, RowWriter> = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };\n\n// Each writer decides where a notice can sit without wrecking the format it travels in; the tail\n// repeat is common to all of them, because `| tail` is how a long read gets inspected.\nfunction writeRows(format: string, rows: Array<Record<string, unknown>>, columns: string[], notices: string[]): void {\n const write = ROW_WRITERS[format] ?? writeMdRows;\n write(rows, columns, notices);\n if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {\n for (const line of notices) writeLine(line);\n }\n}\n\nexport function outputResults(\n rows: Array<Record<string, unknown>>,\n fieldDescs: Record<string, string>,\n warnings: AdsWarning[],\n args: Record<string, unknown>,\n cached: boolean,\n read: ReadContext,\n): void {\n const format = (args.output as string) || \"json\";\n const allWarnings = [\n ...warnings,\n ...completenessWarnings({\n rowCount: rows.length,\n effectiveLimit: read.effectiveLimit,\n hasMore: Boolean(read.nextCursor),\n }),\n ];\n emitToStderr(warningLines(allWarnings, \"csv\"));\n\n if (format === \"json\") {\n writeAdsJson(\n buildQueryEnvelope(rows, fieldDescs, allWarnings, {\n full: Boolean(args.full),\n cached,\n nextCursor: read.nextCursor,\n }),\n );\n return;\n }\n writeRows(format, rows, resultColumns(read.gaql, rows), warningLines(allWarnings, format));\n}\n\n/** The statement typed as the positional or as --gaql — never two different ones. */\nexport function resolveGaqlInput(\n args: Record<string, unknown>,\n): { ok: true; gaql: string } | { ok: false; message: string } {\n const positional = typeof args.query === \"string\" ? args.query.trim() : \"\";\n const flag = typeof args.gaql === \"string\" ? args.gaql.trim() : \"\";\n if (positional && flag && positional !== flag) {\n return { ok: false, message: \"Pass the GAQL statement once — as the positional or as --gaql, not both.\" };\n }\n return { ok: true, gaql: positional || flag };\n}\n\nfunction resolveGaql(args: Record<string, unknown>): string | null {\n if (args.preset) {\n const preset = getPreset(args.preset as string);\n if (!preset) {\n const available = PRESETS.map((p) => p.name).join(\", \");\n writeAdsJson({\n ok: false,\n error: { code: \"VALIDATION_ERROR\", message: `Unknown preset: \"${args.preset}\". Available: ${available}` },\n });\n return null;\n }\n const expanded = expandPreset(preset, {\n dateRange: args[\"date-range\"] as string | undefined,\n includePaused: Boolean(args[\"include-paused\"]),\n });\n if (expanded.dateRangeError) {\n writeAdsJson({\n ok: false,\n error: { code: \"VALIDATION_ERROR\", message: expanded.dateRangeError },\n });\n return null;\n }\n return expanded.query;\n }\n const input = resolveGaqlInput(args);\n if (!input.ok || !input.gaql) {\n writeAdsJson({\n ok: false,\n error: {\n code: \"VALIDATION_ERROR\",\n message: input.ok\n ? \"Provide a GAQL query (positional or --gaql) or use --preset. Run --list-presets to see options.\"\n : input.message,\n },\n });\n return null;\n }\n return input.gaql;\n}\n\n// Resources whose rows only actually serve when their whole status chain is ENABLED, MINUS the\n// multi-rung ones preflight's SERVING_CHAINS already judges rung by rung. What is left is the\n// single-rung resources: they carry no ancestor to forget, so preflight exempts them by design and\n// this is the only place that says an unfiltered read of them returns paused rows. Two warnings for\n// one defect is how an agent learns to skim past both, so the populations must not overlap.\nconst SERVING_HIERARCHY_RESOURCES = [\n \"campaign\",\n \"ad_group\",\n \"ad_group_ad\",\n \"ad_group_criterion\",\n \"keyword_view\",\n \"search_term_view\",\n \"shopping_performance_view\",\n \"asset_group_asset\",\n].filter((resource) => !(resource in SERVING_CHAINS));\n\n// Extract the WHERE clause's predicates from an uppercased GAQL string — everything between\n// WHERE and the first trailing clause (ORDER BY / LIMIT / PARAMETERS). Returns \"\" when there is\n// no WHERE. Used to detect a real status *filter* rather than a status column in the SELECT list.\nfunction whereClause(upperQuery: string): string {\n const whereIdx = upperQuery.indexOf(\" WHERE \");\n if (whereIdx === -1) return \"\";\n let rest = upperQuery.slice(whereIdx + 7);\n for (const trailing of [\" ORDER BY \", \" GROUP BY \", \" LIMIT \", \" PARAMETERS \"]) {\n const idx = rest.indexOf(trailing);\n if (idx !== -1) rest = rest.slice(0, idx);\n }\n return rest;\n}\n\n// Nudge the agent toward serving-only analysis. Presets already default to the serving chain,\n// so this only fires for raw custom GAQL that targets a serving-hierarchy resource without any\n// status predicate — Google Ads has no single serving flag, so an unfiltered query returns\n// paused-parent entities that look live.\nexport function servingScopeWarnings(args: Record<string, unknown>, finalQuery: string): AdsWarning[] {\n // --include-paused only has an effect on presets (expandPreset consumes it). A preset without\n // the flag is serving-only by construction, so it needs no warning; with the flag we say so.\n if (args.preset) {\n // An inventory (`<thing> list`) lists paused entities on purpose: it is where ids to edit come from.\n // A preset with no serving chain has nothing for --include-paused to widen.\n const name = String(args.preset);\n if (!args[\"include-paused\"] || isInventoryPreset(name) || !getPreset(name)?.statusFields?.length) return [];\n return [\n {\n code: \"SERVING_SCOPE\",\n message:\n \"Paused entities are included (--include-paused). Only recommend changes for actually-serving entities (enabled campaign, ad group, and ad) unless the user asked about paused ones.\",\n },\n ];\n }\n\n // Raw GAQL: --include-paused is a no-op here, so don't claim paused rows were included. Warn\n // only when the query targets a serving-hierarchy resource with no status filter in its WHERE\n // (a status column in the SELECT list displays status but doesn't filter on it).\n const upper = finalQuery.toUpperCase();\n const fromMatch = upper.match(/\\bFROM\\s+([A-Z_]+)/);\n const resource = fromMatch?.[1]?.toLowerCase();\n const hierarchical = resource ? SERVING_HIERARCHY_RESOURCES.includes(resource) : false;\n const filtersStatus = whereClause(upper).includes(\".STATUS\");\n if (hierarchical && !filtersStatus) {\n return [\n {\n code: \"SERVING_SCOPE\",\n message:\n \"This query has no status filter, so it may return paused entities that aren't actually serving. Google Ads has no single serving flag — an entity serves only when its whole chain is ENABLED. Add campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED' (as applicable), or use a --preset, unless you intend to include paused entities.\",\n },\n ];\n }\n return [];\n}\n\n// A performance preset Baker's synced copy also answers: say so, since that read has no rate limit.\nfunction syncedCopyWarnings(args: Record<string, unknown>): AdsWarning[] {\n const hint = googlePresetSyncedHint(typeof args.preset === \"string\" ? args.preset : undefined);\n return hint ? [{ code: \"SYNCED_COPY\", message: hint }] : [];\n}\n\n// Parse the SELECT list out of a GAQL statement. Case-insensitive, newline- and\n// whitespace-tolerant. Returns [] when the statement has no parseable SELECT ... FROM.\nexport function parseSelectedFields(gaql: string): string[] {\n const match = gaql.match(/\\bSELECT\\b([\\s\\S]*?)\\bFROM\\b/i);\n const list = match?.[1];\n if (!list) return [];\n return list\n .split(\",\")\n .map((field) => field.trim())\n .filter((field) => /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z0-9_]+)*$/.test(field));\n}\n\n// Which SELECTed fields appeared on NO returned row. Google Ads omits a field entirely from a\n// row when it has no value, so a key's absence is \"not observed\", never \"empty\" — and the union\n// across ALL rows is the only honest denominator (one arbitrary row proves nothing).\nfunction foldObservedFields(rows: Array<Record<string, unknown>>, observed: Set<string>): void {\n for (const row of rows) {\n for (const key of Object.keys(row)) observed.add(key);\n }\n}\n\nexport function unobservedSelectedFields(gaql: string, rows: Array<Record<string, unknown>>): string[] {\n const observed = new Set<string>();\n foldObservedFields(rows, observed);\n return unobservedFields(gaql, observed);\n}\n\n// Same verdict from a folded key set instead of the rows, so a read that streamed to disk can\n// still answer it.\nexport function unobservedFields(gaql: string, observed: Set<string>): string[] {\n return parseSelectedFields(gaql).filter((field) => {\n // The backend strips resource_name from every row, so it is never observable.\n if (field.endsWith(\".resource_name\")) return false;\n if (observed.has(field)) return false;\n // A submessage selected as a whole is observed when any of its flattened leaves came back.\n return ![...observed].some((key) => key.startsWith(`${field}.`));\n });\n}\n\n// An absent field must never read as an empty one. Zero rows is a different, already-obvious\n// signal, so this only fires when the read actually returned data.\nexport function missingFieldWarnings(gaql: string, rows: Array<Record<string, unknown>>): AdsWarning[] {\n const observed = new Set<string>();\n foldObservedFields(rows, observed);\n return missingFieldWarningsFor(gaql, observed, rows.length);\n}\n\nexport function missingFieldWarningsFor(gaql: string, observed: Set<string>, rowCount: number): AdsWarning[] {\n if (rowCount === 0) return [];\n const missing = unobservedFields(gaql, observed);\n if (missing.length === 0) return [];\n return [\n {\n code: \"MISSING_FIELDS\",\n message: `These SELECTed fields came back on no row: ${missing.join(\", \")}. Google Ads omits a field entirely when it has no value, so this does NOT mean the setting is empty — it means this read didn't observe it. Check the level that owns the setting before concluding it is unset.`,\n },\n ];\n}\n\n// A CSV header is fixed on the first page and cannot grow a column later, so a submessage selected\n// whole whose leaves first appear on a later page has nowhere to land in the file. Those values are\n// lost, and MISSING_FIELDS cannot see it (the leaves WERE observed), so the loss is named here.\nexport function unwrittenColumnWarnings(header: string[], observed: Set<string>): AdsWarning[] {\n const written = new Set(header);\n const unwritten = [...observed].filter((key) => !written.has(key));\n if (unwritten.length === 0) return [];\n return [\n {\n code: \"UNWRITTEN_COLUMNS\",\n message: `The file has no column for ${unwritten.join(\", \")}: the header was fixed on the first page, which carried none of them, so their values from later pages are NOT in the file. Do not read those columns as empty. SELECT these fields by name instead of their parent, or write to a .jsonl file, then re-run.`,\n },\n ];\n}\n\n// Google omits primary_status_reasons when a keyword has none, so the decoder can only tell\n// \"nothing to report\" from \"we never asked\" by looking at what the statement selected.\nexport function keywordServingOptions(gaql: string): KeywordServingOptions {\n return { statusReasonsSelected: parseSelectedFields(gaql).includes(\"ad_group_criterion.primary_status_reasons\") };\n}\n\nasync function fetchAllPages(\n gaql: string,\n body: Record<string, unknown>,\n args: Record<string, unknown>,\n servingOptions: KeywordServingOptions,\n): Promise<{\n allRows: Array<Record<string, unknown>>;\n lastPageToken?: string;\n totalRowsWritten: number;\n keywordLimits: KeywordLimits;\n observedFields: Set<string>;\n fileColumns: string[];\n}> {\n let allRows: Array<Record<string, unknown>> = [];\n let pageToken: string | undefined = args.cursor as string | undefined;\n let totalRowsWritten = 0;\n let keywordLimits: KeywordLimits | undefined;\n const observedFields = new Set<string>();\n // One header for the whole file: fixed on the first page, since a CSV cannot grow a column later.\n let fileColumns: string[] | undefined;\n\n do {\n const requestBody: Record<string, unknown> = { ...body };\n if (pageToken) requestBody.pageToken = pageToken;\n\n const response = await apiPost<QueryApiResponse>(\"/api/ads/google/query\", requestBody);\n const rows = response.rows ?? [];\n pageToken = response.pageToken;\n\n if (args.out) {\n const shouldAppend = (args.append as boolean) || totalRowsWritten > 0;\n fileColumns ??= resultColumns(gaql, rows);\n writeRowsToFile(resolve(args.out as string), rows, fileColumns, shouldAppend);\n totalRowsWritten += rows.length;\n // The rows are gone after this — fold their verdicts now or the file summary has nothing\n // to say about the very pull this preset is sized for.\n keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);\n foldObservedFields(rows, observedFields);\n } else {\n allRows = allRows.concat(rows);\n }\n\n if (!args.all) break;\n } while (pageToken);\n\n return {\n allRows,\n lastPageToken: pageToken,\n totalRowsWritten,\n keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),\n observedFields,\n fileColumns: fileColumns ?? [],\n };\n}\n\n/** One cached page: its rows and the cursor to the page after it, so a cache hit still says \"there is more\". */\ninterface CachedPage {\n rows: Array<Record<string, unknown>>;\n cursor?: string;\n}\n\n// A page is cached under the statement, its size and the cursor it starts at — any one of the three\n// changes which rows it holds. An --all read is many pages and never comes from (or goes to) this cache.\nexport function pageCacheKey(customerId: string, query: string, pageSize: number, cursor: string | undefined): string {\n return buildQueryCacheKey(customerId, `${query}\\u0000page=${pageSize}\\u0000cursor=${cursor ?? \"\"}`);\n}\n\nasync function executeQuery(\n finalQuery: string,\n args: Record<string, unknown>,\n customerId: string,\n limit: number,\n warnings: AdsWarning[],\n useCache: boolean,\n cacheKey: string,\n): Promise<void> {\n const body: Record<string, unknown> = { customerId, query: finalQuery, pageSize: limit };\n const managerId = getManagerIdForCustomer(customerId);\n if (managerId) body.managerId = managerId;\n if (!useCache) body.skipCache = true;\n\n const servingOptions = keywordServingOptions(finalQuery);\n const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields, fileColumns } = await fetchAllPages(\n finalQuery,\n body,\n args,\n servingOptions,\n );\n\n if (args.out) {\n const truncation = completenessWarnings({\n rowCount: totalRowsWritten,\n effectiveLimit: effectiveRowLimit(finalQuery),\n hasMore: Boolean(lastPageToken),\n });\n const fileWarnings = [\n ...warnings,\n ...renderKeywordLimits(keywordLimits),\n ...missingFieldWarningsFor(finalQuery, observedFields, totalRowsWritten),\n // Only a CSV has a header to fall out of; json and jsonl files carry every row whole.\n ...(isCsvPath(args.out as string) ? unwrittenColumnWarnings(fileColumns, observedFields) : []),\n ...conversionMixWarnings(finalQuery),\n ...moneyUnitWarnings(parseSelectedFields(finalQuery)),\n ...truncation,\n ];\n emitToStderr(warningLines(fileWarnings, \"csv\"));\n writeAdsJson(\n buildFileSummary({\n file: resolve(args.out as string),\n rows: totalRowsWritten,\n warnings: fileWarnings,\n complete: truncation.length === 0,\n }),\n );\n return;\n }\n\n if (useCache && !args.all && allRows.length > 0) {\n const page: CachedPage = { rows: allRows, ...(lastPageToken ? { cursor: lastPageToken } : {}) };\n cacheSet(\"queries\", cacheKey, page, getQueryTtl(finalQuery), getFieldDescriptions(extractFields(allRows)));\n }\n const fields = extractFields(allRows);\n outputResults(\n allRows,\n getFieldDescriptions(fields),\n [\n ...warnings,\n ...missingFieldWarnings(finalQuery, allRows),\n ...keywordServingWarnings(allRows, servingOptions),\n ...conversionMixWarnings(finalQuery),\n ...moneyUnitWarnings(resultColumns(finalQuery, allRows)),\n ],\n args,\n false,\n {\n gaql: finalQuery,\n effectiveLimit: effectiveRowLimit(finalQuery),\n nextCursor: lastPageToken,\n },\n );\n}\n\n// A --out read must always produce the file. The --out branch never populates the cache, so a hit\n// there can only come from an earlier stdout read of the same statement — serving it would print\n// rows and leave the file stale or absent, which the next command reads as this pull.\nexport function shouldReadCache(args: Record<string, unknown>): boolean {\n return !args[\"no-cache\"] && !args.out && !args.all;\n}\n\ninterface ValidatedArgs {\n customerId: string;\n limit: number;\n finalQuery: string;\n warnings: AdsWarning[];\n useCache: boolean;\n cacheKey: string;\n}\n\nconst DEFAULT_QUERY_LIMIT = 200;\n\n/**\n * `--limit` is the page size, and the server refuses a page above its cap. Checked here, before any\n * request, so an out-of-range value comes back as the input error it is — sent on, the server's\n * refusal reached the agent as an API_ERROR, indistinguishable from a Google failure.\n */\nexport function parseQueryLimit(\n raw: unknown,\n fallback = DEFAULT_QUERY_LIMIT,\n): { ok: true; limit: number } | { ok: false; error: AdsErrorEnvelope } {\n if (raw === undefined || raw === null || raw === \"\") return { ok: true, limit: fallback };\n const limit = Number(raw);\n if (Number.isInteger(limit) && limit >= 1 && limit <= GOOGLE_ADS_QUERY_MAX_PAGE_SIZE) return { ok: true, limit };\n return {\n ok: false,\n error: {\n ok: false,\n error: {\n code: \"VALIDATION_ERROR\",\n message: `--limit must be a whole number from 1 to ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE}, got '${String(raw)}'.`,\n fix: {\n action: \"retry_with_flag\",\n explanation: `--limit is the size of one page, at most ${GOOGLE_ADS_QUERY_MAX_PAGE_SIZE} rows. For more rows, keep the page size and add --all --out <file>: it reads every page.`,\n },\n retryable: false,\n },\n },\n };\n}\n\n/** The page size: --limit when given, else the preset's own page size, else 200. */\nexport function queryPageSize(args: Record<string, unknown>): ReturnType<typeof parseQueryLimit> {\n const preset = typeof args.preset === \"string\" ? getPreset(args.preset) : undefined;\n return parseQueryLimit(args.limit, preset?.defaultLimit ?? DEFAULT_QUERY_LIMIT);\n}\n\nasync function validateArgs(args: Record<string, unknown>): Promise<ValidatedArgs | null> {\n const parsedLimit = queryPageSize(args);\n if (!parsedLimit.ok) {\n writeAdsJson(parsedLimit.error);\n process.exit(1);\n }\n const { limit } = parsedLimit;\n const customerId = await resolveCustomerId(args);\n\n const gaql = resolveGaql(args);\n if (!gaql) {\n process.exit(1);\n }\n\n const preflight = validatePreflight(gaql, customerId);\n if (!preflight.valid && preflight.error) {\n writeAdsJson(preflight.error);\n process.exit(1);\n }\n\n const finalQuery = preflight.correctedQuery ?? gaql;\n return {\n customerId,\n limit,\n finalQuery,\n warnings: [...preflight.warnings, ...servingScopeWarnings(args, finalQuery), ...syncedCopyWarnings(args)],\n useCache: !args[\"no-cache\"],\n cacheKey: pageCacheKey(customerId, finalQuery, limit, args.cursor as string | undefined),\n };\n}\n\nfunction handleQueryError(err: unknown, finalQuery: string, customerId: string): never {\n if (err instanceof ApiError) {\n const envelope = parseApiError(err.message, finalQuery, customerId, err.code);\n envelope.error.gaqlExecuted = finalQuery;\n writeAdsJson(envelope);\n process.exit(1);\n }\n writeAdsJson({\n ok: false,\n error: {\n code: \"NETWORK_ERROR\",\n message: err instanceof Error ? err.message : \"Unexpected error\",\n fix: { action: \"wait_and_retry\", explanation: \"Network error — check connectivity and retry\" },\n retryable: true,\n gaqlExecuted: finalQuery,\n },\n } satisfies AdsErrorEnvelope);\n process.exit(1);\n}\n\n/**\n * One read, start to finish: validate, serve a cached page or fetch, then write the envelope. The\n * `query` command and every `<thing> list` command (which hand it a preset) run through here.\n */\nexport async function runQuery(args: Record<string, unknown>): Promise<void> {\n const validated = await validateArgs(args);\n if (!validated) return;\n const { customerId, limit, finalQuery, warnings, useCache, cacheKey } = validated;\n\n if (shouldReadCache(args)) {\n const cached = cacheGet<CachedPage>(\"queries\", cacheKey);\n if (cached && Array.isArray(cached.data.rows)) {\n const rows = cached.data.rows;\n outputResults(\n rows,\n cached.fields ?? getFieldDescriptions(extractFields(rows)),\n [\n ...warnings,\n ...missingFieldWarnings(finalQuery, rows),\n ...keywordServingWarnings(rows, keywordServingOptions(finalQuery)),\n ...conversionMixWarnings(finalQuery),\n ...moneyUnitWarnings(resultColumns(finalQuery, rows)),\n ],\n args,\n true,\n { gaql: finalQuery, effectiveLimit: effectiveRowLimit(finalQuery), nextCursor: cached.data.cursor },\n );\n return;\n }\n }\n\n try {\n await executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey);\n } catch (err) {\n handleQueryError(err, finalQuery, customerId);\n }\n}\n\nexport const queryCommand = defineCommand({\n meta: {\n name: \"query\",\n description: `Run GAQL against Google Ads, one page at a time. Supports raw GAQL, presets, paging, file export, and caching.\n\nStart here:\n baker ads google query --list-presets\n baker ads google query --preset campaign-performance --customer-id 1234567890\n\nEach read returns one page (--limit rows; the preset's page size by default, else 200). When the\naccount holds more, the envelope carries pagination.cursor: pass it with --cursor for the next page,\nor read everything with --all --out <file>. A LIMIT written in the GAQL caps the whole read.\n\nExamples:\n baker ads google query \"SELECT campaign.name, metrics.clicks FROM campaign WHERE segments.date DURING LAST_7_DAYS\" --customer-id 1234567890\n baker ads google query --gaql \"SELECT ad_group.name FROM ad_group WHERE ad_group.status = 'ENABLED'\" --customer-id 1234567890\n baker ads google query --preset search-terms --customer-id 1234567890 --date-range LAST_7_DAYS --all --out terms.csv\n baker ads google query --preset keyword-serving --customer-id 1234567890 --cursor <pagination.cursor>`,\n },\n args: {\n query: { type: \"positional\", description: \"GAQL SELECT statement\", required: false },\n gaql: {\n type: \"string\",\n description: \"GAQL SELECT statement as a flag (instead of the positional)\",\n required: false,\n },\n \"customer-id\": { type: \"string\", description: \"Google Ads customer ID (10 digits, no dashes)\", required: false },\n preset: { type: \"string\", description: \"Named query preset\", required: false },\n \"date-range\": { type: \"string\", description: \"Date range override for presets\", required: false },\n \"include-paused\": {\n type: \"boolean\",\n description: \"Include paused entities in preset results (default: serving entities only)\",\n required: false,\n },\n limit: {\n type: \"string\",\n description: \"Rows per page (default: the preset's page size, else 200)\",\n required: false,\n },\n \"list-presets\": { type: \"boolean\", description: \"List available presets\", required: false },\n cursor: { type: \"string\", description: \"pagination.cursor from the previous page\", required: false },\n all: { type: \"boolean\", description: \"Follow every page (use with --out for large reads)\", required: false },\n out: { type: \"string\", description: \"File path for output\", required: false },\n append: { type: \"boolean\", description: \"Append to file\", required: false },\n output: { type: \"string\", description: \"Format: json|csv|jsonl|md\", required: false, default: \"json\" },\n \"no-cache\": { type: \"boolean\", description: \"Skip cache\", required: false },\n full: { type: \"boolean\", description: \"Include the field description glossary\", required: false },\n },\n run: async ({ args }) => {\n if (args[\"list-presets\"]) {\n writeAdsJson({\n ok: true,\n data: PRESETS.map((p) => ({\n name: p.name,\n description: p.description,\n defaultDateRange: p.defaultDateRange,\n pageSize: p.defaultLimit,\n servingOnly: (p.statusFields?.length ?? 0) > 0,\n gaql: expandPreset(p, {}).query,\n })),\n });\n return;\n }\n\n await runQuery(args);\n },\n});\n","import type { PresetDefinition } from \"../types.ts\";\nimport { duringLiteralExplanation, equivalentBetween, isDuringLiteral } from \"./during-literals.ts\";\n\nconst BETWEEN_RE = /^BETWEEN\\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\\s+AND\\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'$/i;\n\n/** What `{dateRange}` may expand to: a literal Google accepts after DURING, or a BETWEEN range. */\nexport function isValidDateRange(value: string): boolean {\n const trimmed = value.trim();\n return isDuringLiteral(trimmed) || BETWEEN_RE.test(trimmed);\n}\n\nfunction dateRangeError(dateRange: string, now: Date): string {\n if (/^[A-Za-z0-9_]+$/.test(dateRange.trim())) {\n return `Invalid date range: \"${dateRange}\". ${duringLiteralExplanation(dateRange, equivalentBetween(dateRange, now))} Pass it as --date-range \"BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'\".`;\n }\n return `Invalid date range: \"${dateRange}\". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`;\n}\n\n/**\n * The status columns that decide whether a row of each resource is actually serving, outermost\n * first. Google Ads has no single serving flag: an entity runs only when every level of its chain\n * is ENABLED, so an ENABLED keyword under a paused ad group is not live.\n *\n * Only resources with **two or more** rungs are listed. A one-rung resource (campaign,\n * campaign_criterion, shopping_performance_view) carries its own status on the row and has no\n * ancestor to forget, which is the whole failure this table exists to catch.\n *\n * Presets expand `{statusScope}` from their own `statusFields`; hand-written GAQL is checked\n * against this table in preflight. `presets.test.ts` asserts the two never drift apart.\n */\nexport const SERVING_CHAINS: Record<string, string[]> = {\n keyword_view: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n ad_group_criterion: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n ad_group_ad: [\"campaign.status\", \"ad_group.status\", \"ad_group_ad.status\"],\n search_term_view: [\"campaign.status\", \"ad_group.status\"],\n ad_group: [\"campaign.status\", \"ad_group.status\"],\n asset_group: [\"campaign.status\", \"asset_group.status\"],\n // An asset link carries its own status, and it is not the campaign's, the ad group's or the asset\n // group's. Google returns a REMOVED link like any other row, so a read scoped only to its owners\n // lists extensions that are no longer attached to anything — which is what 74 detaches were planned\n // from on a live account, every one of them refused at publish as `RESOURCE_NOT_FOUND`.\n // `customer_asset` is deliberately absent: one rung, no ancestor to forget.\n asset_group_asset: [\"campaign.status\", \"asset_group.status\", \"asset_group_asset.status\"],\n campaign_asset: [\"campaign.status\", \"campaign_asset.status\"],\n ad_group_asset: [\"campaign.status\", \"ad_group.status\", \"ad_group_asset.status\"],\n};\n\nconst CORE_PRESETS: PresetDefinition[] = [\n {\n name: \"campaign-performance\",\n description: \"Campaign-level metrics overview\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.bidding_strategy_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.all_conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"budget-limited\",\n description:\n \"Campaigns Google reports as limited by budget, with each one's bid target next to its actual cost per conversion — the state where a target above actual now drives delivery up to the target instead of holding recent cost\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.bidding_strategy_type, campaign.primary_status, campaign.primary_status_reasons, campaign.target_cpa.target_cpa_micros, campaign.maximize_conversions.target_cpa_micros, campaign.target_roas.target_roas, campaign.maximize_conversion_value.target_roas, campaign.target_cpc.target_cpc_micros, campaign_budget.amount_micros, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.cost_per_conversion, metrics.search_budget_lost_impression_share FROM campaign WHERE campaign.primary_status_reasons CONTAINS ANY (BUDGET_CONSTRAINED) AND segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"conversions-by-action\",\n description:\n \"Which conversion action each campaign's conversions actually are — the read that says whether two campaigns' conversion rates are measuring the same event\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, segments.conversion_action_name, segments.conversion_action_category, metrics.conversions, metrics.all_conversions FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.conversions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"keyword-analysis\",\n description:\n \"Keyword performance with match type, quality and the first-page, top-of-page and first-position CPC estimates\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.quality_info.quality_score, ad_group_criterion.position_estimates.first_page_cpc_micros, ad_group_criterion.position_estimates.top_of_page_cpc_micros, ad_group_criterion.position_estimates.first_position_cpc_micros, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.all_conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n },\n {\n name: \"keyword-serving\",\n description:\n \"Why a keyword is limited — Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.primary_status, ad_group_criterion.primary_status_reasons, ad_group_criterion.system_serving_status, ad_group_criterion.approval_status, ad_group_criterion.effective_cpc_bid_micros, ad_group_criterion.effective_cpc_bid_source, ad_group_criterion.position_estimates.first_page_cpc_micros, ad_group_criterion.position_estimates.top_of_page_cpc_micros, ad_group_criterion.quality_info.quality_score, ad_group_criterion.quality_info.creative_quality_score, ad_group_criterion.quality_info.post_click_quality_score, ad_group_criterion.quality_info.search_predicted_ctr FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n },\n {\n name: \"positive-keywords\",\n description: \"Positive (targeting) keywords only — excludes negatives\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n },\n {\n name: \"negative-keywords\",\n description: \"Negative keywords only — campaign and ad group level\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE AND campaign.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"negative-keyword-lists\",\n description:\n \"Shared negative keyword lists and the terms inside them (SharedSet members), with each member's resource name — what `negative-keywords remove` / a list-member removal takes\",\n gaqlTemplate: `SELECT shared_set.id, shared_set.name, shared_criterion.resource_name, shared_criterion.criterion_id, shared_criterion.keyword.text, shared_criterion.keyword.match_type FROM shared_criterion WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND shared_set.status = 'ENABLED' ORDER BY shared_set.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"negative-list-attachments\",\n description: \"Which campaigns each shared negative keyword list is attached to\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, shared_set.id, shared_set.name FROM campaign_shared_set WHERE shared_set.type = 'NEGATIVE_KEYWORDS' AND campaign_shared_set.status = 'ENABLED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"search-terms\",\n description: \"Actual user search queries triggering ads, with the match type that matched them\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, segments.search_term_match_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_7_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"ad-copy-performance\",\n description: \"Every ad's metrics, with its headlines and descriptions when it is a responsive search ad\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_ad.ad.id, ad_group_ad.ad.type, ad_group_ad.status, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.ad.final_urls, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.all_conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_ad.status\"],\n },\n {\n name: \"asset-performance\",\n // `asset_group_asset.performance_label` is gone (v25 answers UNRECOGNIZED_FIELD): Google retired\n // the BEST/GOOD/LOW label, so the read is each asset's serving status and its own metrics.\n description: \"Performance Max assets: serving status and metrics per asset\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.status, asset_group_asset.field_type, asset_group_asset.primary_status, asset_group_asset.primary_status_reasons, asset.type, asset.text_asset.text, asset.image_asset.full_size.url, metrics.impressions, metrics.clicks, metrics.conversions, metrics.cost_micros FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope}`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"asset_group.status\", \"asset_group_asset.status\"],\n },\n {\n name: \"shopping-products\",\n description: \"Product-level shopping performance metrics\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, segments.product_title, segments.product_item_id, segments.product_brand, segments.product_type_l1, metrics.clicks, metrics.impressions, metrics.cost_micros, metrics.conversions FROM shopping_performance_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"account-summary\",\n description: \"Account-level totals for a date range\",\n gaqlTemplate: `SELECT customer.id, customer.descriptive_name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.all_conversions, metrics.conversions_value FROM customer WHERE segments.date DURING {dateRange} LIMIT 1`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 1,\n },\n];\n\n/**\n * Reports the account read most often asks for beyond the core presets: where Performance Max\n * spends, where display ads showed, which RSA assets serve, landing pages, geography, demographics,\n * AI Max matches, the account budget, Google's recommendations and its bid/budget simulations.\n * Every GAQL here was run against a live v25 account before it was added.\n */\nexport const REPORT_PRESETS: PresetDefinition[] = [\n {\n name: \"pmax-channels\",\n description:\n \"Performance Max spend and results split by channel (Search, YouTube, Display, Discover, Gmail, Maps…) — segments.ad_network_type per campaign\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, segments.ad_network_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM campaign WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"pmax-placements\",\n description:\n \"Where Performance Max ads showed on Display / YouTube / apps (Google reports impressions only for these)\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, performance_max_placement_view.display_name, performance_max_placement_view.placement, performance_max_placement_view.placement_type, performance_max_placement_view.target_url, metrics.impressions FROM performance_max_placement_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"placements\",\n description:\n \"Where Display, Video and Demand Gen ads showed — each website, app or YouTube video, with cost and conversions (exclusion candidates)\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, detail_placement_view.display_name, detail_placement_view.placement, detail_placement_view.placement_type, detail_placement_view.target_url, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM detail_placement_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"rsa-assets\",\n description:\n \"Each responsive search ad headline and description: its text, pin, whether it is enabled, its approval and its own impressions/clicks/conversions\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_ad.ad.id, ad_group_ad.status, ad_group_ad_asset_view.field_type, ad_group_ad_asset_view.pinned_field, ad_group_ad_asset_view.enabled, ad_group_ad_asset_view.policy_summary, asset.id, asset.text_asset.text, metrics.impressions, metrics.clicks, metrics.conversions FROM ad_group_ad_asset_view WHERE ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' AND segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_ad.status\"],\n },\n {\n name: \"landing-pages\",\n description:\n \"Final URLs as written on the ads, with clicks, cost, conversions, mobile-friendly click share and speed score\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, landing_page_view.unexpanded_final_url, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.mobile_friendly_clicks_percentage, metrics.speed_score FROM landing_page_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.clicks DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"geo-performance\",\n description:\n \"Results by country and location type (people IN vs INTERESTED IN the place) per campaign — the read behind a Presence vs Presence-or-interest decision\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, geographic_view.country_criterion_id, geographic_view.location_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM geographic_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"age-performance\",\n description: \"Results by age range per ad group\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.age_range.type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM age_range_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"gender-performance\",\n description: \"Results by gender per ad group\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.gender.type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM gender_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"ai-max-search-terms\",\n description:\n \"AI Max for Search: each search term with the headline and landing page Google generated or chose for it, with results\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ai_max_search_term_ad_combination_view.search_term, ai_max_search_term_ad_combination_view.headline, ai_max_search_term_ad_combination_view.landing_page, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM ai_max_search_term_ad_combination_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"bidding-strategy-performance\",\n description: \"Each portfolio bidding strategy's type, status, targets and metrics\",\n gaqlTemplate: `SELECT bidding_strategy.id, bidding_strategy.name, bidding_strategy.type, bidding_strategy.status, bidding_strategy.campaign_count, bidding_strategy.target_cpa.target_cpa_micros, bidding_strategy.target_roas.target_roas, bidding_strategy.maximize_conversions.target_cpa_micros, bidding_strategy.maximize_conversion_value.target_roas, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.all_conversions, metrics.conversions_value FROM bidding_strategy WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC`,\n defaultDateRange: \"LAST_30_DAYS\",\n defaultLimit: 200,\n statusFields: [\"bidding_strategy.status\"],\n },\n {\n name: \"conversion-tags\",\n description:\n \"The tracking code of each website and call conversion action: the Google tag and the event snippet to put on the site\",\n gaqlTemplate: `SELECT conversion_action.id, conversion_action.name, conversion_action.type, conversion_action.status, conversion_action.tag_snippets FROM conversion_action WHERE conversion_action.type IN ('WEBPAGE', 'WEBPAGE_CODELESS', 'WEBSITE_CALL') AND conversion_action.status != 'REMOVED' ORDER BY conversion_action.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 50,\n },\n {\n name: \"account-budget\",\n description:\n \"The account budget (invoiced billing): approved spending limit, served amount, dates and any pending change — read-only\",\n gaqlTemplate: `SELECT account_budget.id, account_budget.name, account_budget.status, account_budget.billing_setup, account_budget.approved_spending_limit_micros, account_budget.approved_spending_limit_type, account_budget.amount_served_micros, account_budget.total_adjustments_micros, account_budget.approved_start_date_time, account_budget.approved_end_date_time, account_budget.pending_proposal.proposal_type, account_budget.pending_proposal.spending_limit_micros FROM account_budget ORDER BY account_budget.approved_start_date_time DESC`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 50,\n },\n {\n name: \"recommendations\",\n description:\n \"Google's open recommendations (not dismissed): type, the campaign/ad group it targets, and its estimated impact\",\n gaqlTemplate: `SELECT recommendation.resource_name, recommendation.type, recommendation.campaign, recommendation.ad_group, recommendation.impact FROM recommendation WHERE recommendation.dismissed = FALSE`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"campaign-simulations\",\n description:\n \"Google's what-if curves per campaign — budget, target CPA / ROAS, CPC bid or target impression share against the clicks, cost and conversions Google predicts\",\n gaqlTemplate: `SELECT campaign_simulation.campaign_id, campaign_simulation.type, campaign_simulation.modification_method, campaign_simulation.start_date, campaign_simulation.end_date, campaign_simulation.budget_point_list.points, campaign_simulation.target_cpa_point_list.points, campaign_simulation.target_roas_point_list.points, campaign_simulation.cpc_bid_point_list.points, campaign_simulation.target_impression_share_point_list.points FROM campaign_simulation`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"keyword-bid-simulations\",\n description: \"Google's what-if curve per keyword: CPC bid against predicted clicks, cost and impressions\",\n gaqlTemplate: `SELECT ad_group_criterion_simulation.ad_group_id, ad_group_criterion_simulation.criterion_id, ad_group_criterion_simulation.type, ad_group_criterion_simulation.start_date, ad_group_criterion_simulation.end_date, ad_group_criterion_simulation.cpc_bid_point_list.points FROM ad_group_criterion_simulation`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n];\n\n/**\n * What each `<thing> list` command reads: every entity of that kind that is not removed, with the\n * id or resource name its write commands take. Read through `query`, so they page like any read.\n * Run as `query --preset <name>` they are serving-only like every preset; `<thing> list` widens the\n * chain to everything not removed, because the thing you came to edit is often paused.\n */\nexport const INVENTORY_PRESETS: PresetDefinition[] = [\n {\n name: \"campaigns\",\n description: \"Every campaign: id, name, status, channel, bidding, budget and dates\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.primary_status, campaign.advertising_channel_type, campaign.advertising_channel_sub_type, campaign.bidding_strategy_type, campaign.bidding_strategy, campaign.campaign_budget, campaign_budget.amount_micros, campaign.start_date_time, campaign.end_date_time FROM campaign{statusWhere} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\"],\n },\n {\n name: \"budgets\",\n description:\n \"Every campaign budget: id, name, daily amount, delivery, whether it is shared and how many campaigns use it\",\n gaqlTemplate: `SELECT campaign_budget.id, campaign_budget.name, campaign_budget.status, campaign_budget.amount_micros, campaign_budget.delivery_method, campaign_budget.explicitly_shared, campaign_budget.reference_count FROM campaign_budget WHERE campaign_budget.status != 'REMOVED' ORDER BY campaign_budget.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"ad-groups\",\n description: \"Every ad group: id, name, status, type and default CPC bid, under its campaign\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group.type, ad_group.cpc_bid_micros FROM ad_group{statusWhere} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\"],\n },\n {\n name: \"keywords\",\n description:\n \"Every positive keyword: resource name, text, match type, status, bid and final URL — `keywords update|remove` take the resource name\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.resource_name, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.negative, ad_group_criterion.cpc_bid_micros, ad_group_criterion.final_urls FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n },\n {\n name: \"campaign-negative-keywords\",\n description: \"Campaign-level negative keywords with the resource name `negative-keywords remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign_criterion.resource_name, campaign_criterion.keyword.text, campaign_criterion.keyword.match_type FROM campaign_criterion WHERE campaign_criterion.type = 'KEYWORD' AND campaign_criterion.negative = TRUE AND campaign_criterion.status != 'REMOVED' AND campaign.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"ad-group-negative-keywords\",\n description: \"Ad-group-level negative keywords with the resource name `negative-keywords remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.resource_name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.negative FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE AND ad_group_criterion.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"shared-sets\",\n description:\n \"Every shared list (negative keyword / placement lists): id, name, type, member count and how many campaigns use it\",\n gaqlTemplate: `SELECT shared_set.id, shared_set.name, shared_set.type, shared_set.status, shared_set.member_count, shared_set.reference_count FROM shared_set WHERE shared_set.status != 'REMOVED' ORDER BY shared_set.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"ads\",\n description: \"Every ad: id, type, name, status, approval and final URLs, under its ad group\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_ad.resource_name, ad_group_ad.ad.id, ad_group_ad.ad.type, ad_group_ad.ad.name, ad_group_ad.status, ad_group_ad.policy_summary.approval_status, ad_group_ad.ad.final_urls FROM ad_group_ad{statusWhere} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_ad.status\"],\n },\n {\n name: \"assets\",\n description: \"Every asset in the account library: id, name, type, text and final URLs\",\n gaqlTemplate: `SELECT asset.id, asset.name, asset.type, asset.text_asset.text, asset.sitelink_asset.link_text, asset.callout_asset.callout_text, asset.image_asset.full_size.url, asset.youtube_video_asset.youtube_video_id, asset.final_urls FROM asset ORDER BY asset.type`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"asset-groups\",\n description: \"Every Performance Max asset group: id, name, status, final URLs and strength, under its campaign\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.resource_name, asset_group.id, asset_group.name, asset_group.status, asset_group.final_urls, asset_group.ad_strength FROM asset_group{statusWhere} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n statusFields: [\"campaign.status\", \"asset_group.status\"],\n },\n {\n name: \"audiences\",\n description:\n \"Every audience (user list): id, name, type, membership status, size on search and display, membership duration\",\n gaqlTemplate: `SELECT user_list.id, user_list.name, user_list.type, user_list.membership_status, user_list.size_for_search, user_list.size_for_display, user_list.membership_life_span, user_list.eligible_for_search, user_list.eligible_for_display FROM user_list ORDER BY user_list.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"conversion-actions\",\n description: \"Every conversion action: id, name, type, category, origin, status, primary or secondary, counting\",\n gaqlTemplate: `SELECT conversion_action.id, conversion_action.name, conversion_action.type, conversion_action.category, conversion_action.origin, conversion_action.status, conversion_action.primary_for_goal, conversion_action.counting_type FROM conversion_action WHERE conversion_action.status != 'REMOVED' ORDER BY conversion_action.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"bidding-strategies\",\n description: \"Every portfolio bidding strategy: id, name, type, status and how many campaigns use it\",\n gaqlTemplate: `SELECT bidding_strategy.id, bidding_strategy.name, bidding_strategy.type, bidding_strategy.status, bidding_strategy.campaign_count FROM bidding_strategy WHERE bidding_strategy.status != 'REMOVED' ORDER BY bidding_strategy.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"labels\",\n description: \"Every label: id, name, colour and description\",\n gaqlTemplate: `SELECT label.id, label.name, label.status, label.text_label.background_color, label.text_label.description FROM label WHERE label.status != 'REMOVED' ORDER BY label.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"merchant-center\",\n description:\n \"Merchant Center accounts linked to this Google Ads account — the merchantId a Shopping or retail Performance Max campaign names\",\n gaqlTemplate: `SELECT product_link.resource_name, product_link.product_link_id, product_link.type, product_link.merchant_center.merchant_center_id FROM product_link WHERE product_link.type = 'MERCHANT_CENTER'`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 200,\n },\n {\n name: \"campaign-criteria\",\n description:\n \"Every campaign targeting criterion and exclusion — locations, languages, devices, ad schedules, radius, carriers, operating systems, placements, topics, demographics, content types, IP addresses, web pages, brand lists, audiences — with the resource name `campaign-criteria update|remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign_criterion.resource_name, campaign_criterion.type, campaign_criterion.negative, campaign_criterion.bid_modifier, campaign_criterion.location.geo_target_constant, campaign_criterion.language.language_constant, campaign_criterion.device.type, campaign_criterion.ad_schedule.day_of_week, campaign_criterion.ad_schedule.start_hour, campaign_criterion.ad_schedule.end_hour, campaign_criterion.proximity.radius, campaign_criterion.placement.url, campaign_criterion.mobile_application.app_id, campaign_criterion.youtube_channel.channel_id, campaign_criterion.youtube_video.video_id, campaign_criterion.topic.topic_constant, campaign_criterion.age_range.type, campaign_criterion.gender.type, campaign_criterion.income_range.type, campaign_criterion.parental_status.type, campaign_criterion.content_label.type, campaign_criterion.ip_block.ip_address, campaign_criterion.webpage.criterion_name, campaign_criterion.brand_list.shared_set, campaign_criterion.carrier.carrier_constant, campaign_criterion.operating_system_version.operating_system_version_constant, campaign_criterion.user_list.user_list, campaign_criterion.user_interest.user_interest_category FROM campaign_criterion WHERE campaign_criterion.type != 'KEYWORD' AND campaign_criterion.status != 'REMOVED' AND campaign.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"ad-group-criteria\",\n description:\n \"Every ad group targeting criterion and exclusion other than keywords — placements, apps, YouTube, topics, demographics, web pages, Demand Gen locations, languages and audience, audiences — with its bid adjustment, max CPC, status and the resource name `ad-group-criteria update|remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.resource_name, ad_group_criterion.type, ad_group_criterion.negative, ad_group_criterion.status, ad_group_criterion.bid_modifier, ad_group_criterion.cpc_bid_micros, ad_group_criterion.placement.url, ad_group_criterion.mobile_application.app_id, ad_group_criterion.youtube_channel.channel_id, ad_group_criterion.youtube_video.video_id, ad_group_criterion.topic.topic_constant, ad_group_criterion.age_range.type, ad_group_criterion.gender.type, ad_group_criterion.parental_status.type, ad_group_criterion.income_range.type, ad_group_criterion.webpage.criterion_name, ad_group_criterion.location.geo_target_constant, ad_group_criterion.language.language_constant, ad_group_criterion.audience.audience, ad_group_criterion.brand_list.shared_set, ad_group_criterion.user_list.user_list, ad_group_criterion.user_interest.user_interest_category FROM ad_group_criterion WHERE ad_group_criterion.type NOT IN ('KEYWORD', 'LISTING_GROUP'){statusScope} ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n statusFields: [\"campaign.status\", \"ad_group.status\", \"ad_group_criterion.status\"],\n },\n {\n name: \"account-exclusions\",\n description:\n \"Every account-wide exclusion — sites, apps, app categories, YouTube, content types, IP addresses, account negative keyword lists — with the resource name `account-exclusions remove` takes\",\n gaqlTemplate: `SELECT customer_negative_criterion.resource_name, customer_negative_criterion.type, customer_negative_criterion.placement.url, customer_negative_criterion.mobile_application.app_id, customer_negative_criterion.mobile_app_category.mobile_app_category_constant, customer_negative_criterion.youtube_channel.channel_id, customer_negative_criterion.youtube_video.video_id, customer_negative_criterion.content_label.type, customer_negative_criterion.ip_block.ip_address, customer_negative_criterion.negative_keyword_list.shared_set FROM customer_negative_criterion`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"asset-group-signals\",\n description:\n \"Every Performance Max asset group's signals — search themes and the combined audience — under its campaign and group, with the resource name `asset-group-signals remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, asset_group.id, asset_group.name, asset_group_signal.resource_name, asset_group_signal.search_theme.text, asset_group_signal.audience.audience, asset_group_signal.approval_status FROM asset_group_signal WHERE campaign.status != 'REMOVED' AND asset_group.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n {\n name: \"ad-group-bid-modifiers\",\n description:\n \"Every ad group's own device bid adjustment, with the resource name `ad-group-bid-modifiers update|remove` takes\",\n gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_bid_modifier.resource_name, ad_group_bid_modifier.device.type, ad_group_bid_modifier.bid_modifier FROM ad_group_bid_modifier WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' ORDER BY campaign.name`,\n defaultDateRange: \"ALL_TIME\",\n defaultLimit: 500,\n },\n];\n\nexport const PRESETS: PresetDefinition[] = [...CORE_PRESETS, ...REPORT_PRESETS, ...INVENTORY_PRESETS];\n\nexport function isInventoryPreset(name: string): boolean {\n return INVENTORY_PRESETS.some((preset) => preset.name === name);\n}\n\nexport function getPreset(name: string): PresetDefinition | undefined {\n return PRESETS.find((p) => p.name === name);\n}\n\n/**\n * Build the chained status predicate a preset's `{statusScope}` placeholder expands to.\n * Serving-only (default) requires every level `= 'ENABLED'`; includePaused widens to\n * `!= 'REMOVED'` (keeps paused, still drops removed). Returns a leading-AND fragment so it\n * appends cleanly onto an existing WHERE clause, or \"\" when the preset has no serving chain.\n */\nexport function buildStatusScope(statusFields: string[] | undefined, includePaused: boolean): string {\n if (!statusFields || statusFields.length === 0) return \"\";\n const predicate = includePaused ? \"!= 'REMOVED'\" : \"= 'ENABLED'\";\n return statusFields.map((field) => ` AND ${field} ${predicate}`).join(\"\");\n}\n\nexport function expandPreset(\n preset: PresetDefinition,\n params: { dateRange?: string; includePaused?: boolean },\n now: Date = new Date(),\n): { query: string; dateRangeError?: string } {\n const dateRange = params.dateRange ?? preset.defaultDateRange;\n\n // A preset with no date filter (default ALL_TIME) never expands the range, so there is nothing to check.\n if (preset.gaqlTemplate.includes(\"{dateRange}\") && !isValidDateRange(dateRange)) {\n return { query: \"\", dateRangeError: dateRangeError(dateRange, now) };\n }\n\n const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);\n // A literal follows DURING; a range replaces it — \"DURING BETWEEN …\" is a statement Google refuses.\n const range = dateRange.trim();\n const query = preset.gaqlTemplate\n .replace(/DURING\\s+\\{dateRange\\}/g, BETWEEN_RE.test(range) ? range : `DURING ${range}`)\n .replace(/\\{dateRange\\}/g, range)\n .replace(/\\{statusScope\\}/g, statusScope)\n // A template with no WHERE of its own takes the chain as its whole WHERE clause.\n .replace(/\\{statusWhere\\}/g, statusScope ? ` WHERE${statusScope.replace(/^ AND/, \"\")}` : \"\");\n return { query };\n}\n","import type { AdsErrorEnvelope, AdsWarning, PreflightResult } from \"../types.ts\";\nimport { duringLiteralExplanation, equivalentBetween, isDuringLiteral } from \"./during-literals.ts\";\nimport { LIST_FIELDS, listFieldGuidance } from \"./list-fields.ts\";\nimport { SERVING_CHAINS } from \"./presets.ts\";\n\nfunction buildCommand(query: string, customerId: string): string {\n return `baker ads google query \"${query}\" --customer-id ${customerId}`;\n}\n\n// A rung is also settled when the query pins that level to one named entity: the caller looked at\n// that level and chose it, which is a different act from sweeping it silently.\nconst RUNG_PIN_FIELDS: Record<string, string[]> = {\n \"campaign.status\": [\"campaign.id\", \"campaign.resource_name\"],\n \"ad_group.status\": [\"ad_group.id\", \"ad_group.resource_name\"],\n \"ad_group_criterion.status\": [\"ad_group_criterion.criterion_id\", \"ad_group_criterion.resource_name\"],\n \"ad_group_ad.status\": [\"ad_group_ad.ad.id\", \"ad_group_ad.resource_name\"],\n \"asset_group.status\": [\"asset_group.id\", \"asset_group.resource_name\"],\n \"asset_group_asset.status\": [\"asset_group_asset.resource_name\"],\n \"campaign_asset.status\": [\"campaign_asset.resource_name\"],\n \"ad_group_asset.status\": [\"ad_group_asset.resource_name\"],\n};\n\n// WHERE clause only — a status column in the SELECT list displays status, it does not scope the rows.\nfunction whereClause(query: string): string {\n const match = query.match(/\\bWHERE\\b([\\s\\S]*)$/i);\n if (!match?.[1]) return \"\";\n let rest = match[1];\n for (const trailing of [/\\bORDER\\s+BY\\b/i, /\\bLIMIT\\b/i, /\\bPARAMETERS\\b/i]) {\n const idx = rest.search(trailing);\n if (idx !== -1) rest = rest.slice(0, idx);\n }\n return rest;\n}\n\nfunction selectClause(query: string): string {\n return query.split(/\\bFROM\\b/i)[0] ?? \"\";\n}\n\nfunction mentions(clause: string, field: string): boolean {\n return new RegExp(`(?<![\\\\w.])${field.replace(/\\./g, \"\\\\.\")}\\\\b`, \"i\").test(clause);\n}\n\ninterface ChainAnalysis {\n chain: string[];\n /** Rungs the WHERE clause says nothing about — neither a status predicate nor an entity pin. */\n unscoped: string[];\n /** Rungs the WHERE clause pins to `= 'ENABLED'`, i.e. where the query claims \"this is live\". */\n assertsServing: boolean;\n}\n\nfunction analyzeServingChain(query: string): ChainAnalysis | null {\n const resource = fromResource(query);\n const chain = resource ? SERVING_CHAINS[resource] : undefined;\n if (!chain) return null;\n\n const where = whereClause(query);\n\n // A negative keyword never serves, so the serving chain is not a question anyone is asking of it.\n if (/ad_group_criterion\\.negative\\s*=\\s*TRUE/i.test(where)) return null;\n\n const unscoped = chain.filter((rung) => {\n if (mentions(where, rung)) return false;\n return !(RUNG_PIN_FIELDS[rung] ?? []).some((pin) => mentions(where, pin));\n });\n const assertsServing = chain.some((rung) =>\n new RegExp(`${rung.replace(/\\./g, \"\\\\.\")}\\\\s*=\\\\s*'ENABLED'`, \"i\").test(where),\n );\n\n return { chain, unscoped, assertsServing };\n}\n\n/**\n * REJECT a half-asserted chain. A query that pins one rung to ENABLED and leaves another silent\n * returns rows that read as \"these are live\" and are not — the shape that reported 486 serving\n * keywords when the account had 53. There is no reading of \"enabled criteria in ad groups I did\n * not look at\" that anyone means, so this is refused rather than warned: a warning on this path is\n * dropped by `--output csv` and swallowed by `2>/dev/null`, and no data at all is the only signal\n * that survives.\n */\nfunction rejectPartialChain(query: string, customerId: string, analysis: ChainAnalysis): AdsErrorEnvelope | null {\n if (!analysis.assertsServing || analysis.unscoped.length === 0) return null;\n\n const added = analysis.unscoped.map((rung) => `${rung} = 'ENABLED'`).join(\" AND \");\n // The rungs go into SELECT as well where the resource segments on them — on campaign_asset and\n // ad_group_asset a filtered-but-unselected ancestor is refused outright, so a corrected command\n // that only touched the WHERE clause was one the agent could not run either.\n const fixed = addRequiredAncestorFields(query.replace(/\\bWHERE\\b/i, `WHERE ${added} AND`)).query;\n return {\n ok: false,\n error: {\n code: \"INCOMPLETE_STATUS_CHAIN\",\n message: `This query filters on serving status but leaves ${analysis.unscoped.join(\", \")} unscoped. Google Ads has no single serving flag — a row is live only when its whole chain (${analysis.chain.join(\" → \")}) is ENABLED, so rows under a paused parent will come back looking live.`,\n fix: {\n action: \"retry_with_modified_query\",\n correctedCommand: buildCommand(fixed, customerId),\n explanation: `Add the missing rungs: ${added}. To look at paused entities on purpose, scope every rung explicitly instead (e.g. ${analysis.unscoped.map((rung) => `${rung} != 'REMOVED'`).join(\" AND \")}), or use --preset ... --include-paused.`,\n },\n retryable: false,\n },\n };\n}\n\n/**\n * ANNOTATE an unasserted chain. A sweep that claims nothing about serving is a legitimate read —\n * \"show me everything, paused included\" — so the rows are not changed. Every unscoped rung's status\n * column is added to SELECT instead, so the status travels with the data through a CSV, a `--out`\n * file and a `python3` pass. A banner does not survive that trip; a column name does.\n */\nfunction annotateUnscopedChain(query: string, analysis: ChainAnalysis): { query: string; warnings: AdsWarning[] } {\n if (analysis.assertsServing || analysis.unscoped.length === 0) return { query, warnings: [] };\n\n const select = selectClause(query);\n const missing = analysis.unscoped.filter((rung) => !mentions(select, rung));\n if (missing.length === 0) return { query, warnings: [] };\n\n return {\n query: query.replace(/SELECT\\s+/i, `SELECT ${missing.join(\", \")}, `),\n warnings: [\n {\n code: \"SERVING_STATUS_ADDED\",\n message: `Added ${missing.join(\", \")} to SELECT — this query does not scope ${analysis.unscoped.join(\", \")}, so it returns paused-parent rows that are not serving. Read those columns before calling any row live; filter every rung = 'ENABLED' to get serving rows only.`,\n },\n ],\n };\n}\n\n// Resources that return exactly one row per account, so no LIMIT can truncate them and no row can be\n// set against another. SELECTing a segment (segments.date, segments.device …) fans one back out into\n// many rows; filtering on a date range in WHERE does not — Google sums the range into the one row,\n// which is exactly what the account-summary preset reads.\nconst SINGLE_ROW_RESOURCES = [\"customer\"];\n\n// The resource named in the FROM clause, lowercased. Newline- and case-tolerant; null when the\n// statement has no FROM.\nexport function fromResource(query: string): string | null {\n const match = query.match(/\\bFROM\\s+([A-Za-z_][A-Za-z0-9_]*)/i);\n return match?.[1]?.toLowerCase() ?? null;\n}\n\n// A statement that returns one row by construction — decided from the statement, never from the rows.\nexport function isSingleRowRead(query: string): boolean {\n const resource = fromResource(query);\n if (!resource || !SINGLE_ROW_RESOURCES.includes(resource)) return false;\n return !/\\bsegments\\./i.test(selectClause(query));\n}\n\n// Resources whose ancestors SEGMENT them: one budget backs many campaigns, one asset is linked from\n// many campaigns/ad groups, so an ancestor field used in WHERE must appear in SELECT too or Google\n// refuses the whole statement with EXPECTED_REFERENCED_FIELD_IN_SELECT_CLAUSE. Verified live on v23:\n// FROM campaign/ad_group/ad_group_ad/ad_group_criterion/asset_group_asset filter their ancestors\n// freely; these three do not.\nconst SEGMENTING_ANCESTORS: Record<string, string[]> = {\n campaign_budget: [\"campaign\"],\n campaign_asset: [\"campaign\"],\n ad_group_asset: [\"campaign\", \"ad_group\"],\n};\n\n/** The ancestor fields a query filters on but does not select — empty unless the resource segments. */\nfunction missingRequiredSelectFields(query: string): string[] {\n const ancestors = SEGMENTING_ANCESTORS[fromResource(query) ?? \"\"];\n if (!ancestors) return [];\n\n const select = selectClause(query);\n // Everything after WHERE, ORDER BY included — an ancestor field is just as referenced there.\n const where = query.split(/\\bWHERE\\b/i)[1] ?? \"\";\n const filtered = ancestors.flatMap(\n (ancestor) => where.match(new RegExp(`(?<![\\\\w.])${ancestor}\\\\.[\\\\w.]+`, \"g\")) ?? [],\n );\n return [...new Set(filtered)].filter((field) => !mentions(select, field));\n}\n\nfunction addRequiredAncestorFields(query: string): { query: string; warnings: AdsWarning[] } {\n const missing = missingRequiredSelectFields(query);\n if (missing.length === 0) return { query, warnings: [] };\n\n return {\n query: query.replace(/SELECT\\s+/i, `SELECT ${missing.join(\", \")}, `),\n warnings: [\n {\n code: \"REQUIRED_FIELD_ADDED\",\n message: `Added ${missing.join(\", \")} to SELECT — ${fromResource(query)} queries must select the ancestor fields used in WHERE`,\n },\n ],\n };\n}\n\nfunction applyAutoFixes(query: string): { query: string; warnings: AdsWarning[] } {\n let corrected = query;\n const warnings: AdsWarning[] = [];\n\n // bare keyword.text → ad_group_criterion.keyword.text\n // Lookbehind skips fields already scoped to a resource (shared_criterion.keyword.text,\n // campaign_criterion.keyword.text, ad_group_criterion.keyword.text) so they pass through untouched.\n if (/(?<![\\w.])keyword\\.text\\b/.test(corrected)) {\n corrected = corrected.replace(/(?<![\\w.])keyword\\.text\\b/g, \"ad_group_criterion.keyword.text\");\n warnings.push({ code: \"FIELD_RENAMED\", message: \"keyword.text → ad_group_criterion.keyword.text\" });\n }\n\n // bare keyword.match_type → ad_group_criterion.keyword.match_type\n if (/(?<![\\w.])keyword\\.match_type\\b/.test(corrected)) {\n corrected = corrected.replace(/(?<![\\w.])keyword\\.match_type\\b/g, \"ad_group_criterion.keyword.match_type\");\n warnings.push({ code: \"FIELD_RENAMED\", message: \"keyword.match_type → ad_group_criterion.keyword.match_type\" });\n }\n\n // shopping_performance_view.product_* → segments.product_*\n if (/shopping_performance_view\\.product_\\w+/.test(corrected)) {\n corrected = corrected.replace(/shopping_performance_view\\.product_(\\w+)/g, \"segments.product_$1\");\n warnings.push({ code: \"FIELD_RENAMED\", message: \"shopping_performance_view.product_* → segments.product_*\" });\n }\n\n // asset.<type>_asset.final_urls → asset.final_urls (final URLs live on the asset itself;\n // the typed sub-message only carries type-specific fields like link_text/descriptions)\n if (/asset\\.\\w+_asset\\.final_(mobile_)?urls\\b/.test(corrected)) {\n corrected = corrected.replace(/asset\\.\\w+_asset\\.final_(mobile_)?urls\\b/g, \"asset.final_$1urls\");\n warnings.push({ code: \"FIELD_RENAMED\", message: \"asset.<type>_asset.final_urls → asset.final_urls\" });\n }\n\n const ancestorFix = addRequiredAncestorFields(corrected);\n corrected = ancestorFix.query;\n warnings.push(...ancestorFix.warnings);\n\n // campaign.status = 'ACTIVE' → 'ENABLED'\n if (/campaign\\.status\\s*=\\s*'ACTIVE'/i.test(corrected)) {\n corrected = corrected.replace(/campaign\\.status\\s*=\\s*'ACTIVE'/gi, \"campaign.status = 'ENABLED'\");\n warnings.push({ code: \"ENUM_CORRECTED\", message: \"campaign.status ACTIVE → ENABLED\" });\n }\n\n // ad_group.status = 'ACTIVE' → 'ENABLED'\n if (/ad_group\\.status\\s*=\\s*'ACTIVE'/i.test(corrected)) {\n corrected = corrected.replace(/ad_group\\.status\\s*=\\s*'ACTIVE'/gi, \"ad_group.status = 'ENABLED'\");\n warnings.push({ code: \"ENUM_CORRECTED\", message: \"ad_group.status ACTIVE → ENABLED\" });\n }\n\n return { query: corrected, warnings };\n}\n\nconst DURING_CLAUSE = /(?<=[a-z_]\\.[a-z_.]+\\s+)DURING\\s+([A-Za-z0-9_]+)/gi;\n\n/**\n * REJECT a DURING literal outside Google's set (LAST_90_DAYS, LAST_12_MONTHS …). Google refuses the\n * whole statement, so it is refused here with the same window spelled as a BETWEEN range.\n */\nfunction rejectUnsupportedDuring(query: string, customerId: string): AdsErrorEnvelope | null {\n const unsupported = [...query.matchAll(DURING_CLAUSE)]\n .map((match) => match[1] ?? \"\")\n .filter((literal) => !isDuringLiteral(literal));\n const literal = unsupported[0];\n if (literal === undefined) return null;\n const now = new Date();\n const between = equivalentBetween(literal, now);\n // Every unsupported clause is rewritten, so the corrected command runs as-is; one with no fixed\n // window leaves nothing runnable to offer.\n const fixed = unsupported.every((l) => equivalentBetween(l, now) !== null)\n ? query.replace(DURING_CLAUSE, (clause, l: string) =>\n isDuringLiteral(l) ? clause : (equivalentBetween(l, now) ?? clause),\n )\n : undefined;\n return {\n ok: false,\n error: {\n code: \"INVALID_DATE_RANGE\",\n message: `DURING ${literal} is not a Google Ads date range, so Google would refuse this query.`,\n fix: {\n action: \"narrow_date_range\",\n ...(fixed ? { correctedCommand: buildCommand(fixed, customerId) } : {}),\n explanation: duringLiteralExplanation(literal, between),\n },\n retryable: false,\n },\n };\n}\n\nconst WHERE_OPERATOR =\n /(?<![\\w.])([a-z_]+(?:\\.[a-z_]+)+)\\s*(CONTAINS\\s+(?:ALL|ANY|NONE)|IS\\s+NOT\\s+NULL|IS\\s+NULL|NOT\\s+LIKE|NOT\\s+IN|NOT\\s+REGEXP_MATCH|REGEXP_MATCH|LIKE|IN|BETWEEN|DURING|!=|<=|>=|=|<|>)/gi;\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/** The statement without one null test on `field`, with `field` kept in SELECT so the rows still carry it. */\nfunction withoutNullTest(query: string, field: string): string {\n const test = `${escapeRegExp(field)}\\\\s+IS\\\\s+(?:NOT\\\\s+)?NULL\\\\b`;\n const dropped = query\n .replace(new RegExp(`\\\\bWHERE\\\\s+${test}\\\\s+AND\\\\s+`, \"i\"), \"WHERE \")\n .replace(new RegExp(`\\\\s+AND\\\\s+${test}`, \"i\"), \"\")\n .replace(new RegExp(`\\\\s*\\\\bWHERE\\\\s+${test}`, \"i\"), \"\");\n return mentions(selectClause(dropped), field) ? dropped : dropped.replace(/SELECT\\s+/i, `SELECT ${field}, `);\n}\n\n/** `field = 'x'` / `IN (…)` as CONTAINS ANY, `!=` / `NOT IN` as CONTAINS NONE — null when there is no list equivalent. */\nfunction asListCondition(query: string, field: string): string | null {\n const condition = new RegExp(\n `(?<![\\\\w.])${escapeRegExp(field)}\\\\s*(NOT\\\\s+IN\\\\b|IN\\\\b|!=|=)\\\\s*('[^']*'|\\\\([^)]*\\\\))`,\n \"i\",\n );\n const match = query.match(condition);\n if (!match) return null;\n const [, operator = \"\", value = \"\"] = match;\n const contains = /^(=|IN)$/i.test(operator.trim()) ? \"CONTAINS ANY\" : \"CONTAINS NONE\";\n const values = value.startsWith(\"(\") ? value : `(${value})`;\n return query.replace(condition, `${field} ${contains} ${values}`);\n}\n\n/**\n * REJECT an operator a list field does not take. Google filters a list field only with CONTAINS\n * ALL/ANY/NONE and refuses the whole statement otherwise. The corrected read rewrites =/IN/!=/NOT IN\n * to CONTAINS, and drops a null test — which has no list equivalent — keeping the field in SELECT for\n * the caller to filter. LIKE and the rest have no rewrite, so they get the explanation alone.\n */\nfunction rejectListFieldOperator(query: string, customerId: string): AdsErrorEnvelope | null {\n const where = whereClause(query).replace(/'[^']*'/g, \"''\");\n const refusals = [...where.matchAll(WHERE_OPERATOR)]\n .map(([, field = \"\", operator = \"\"]) => ({\n field: field.toLowerCase(),\n operator: operator.toUpperCase().replace(/\\s+/g, \" \"),\n }))\n .filter(({ field, operator }) => LIST_FIELDS.has(field) && !operator.startsWith(\"CONTAINS\"));\n const first = refusals[0];\n if (!first) return null;\n const fixed = refusals.reduce<string | null>(\n (corrected, { field, operator }) =>\n corrected === null\n ? null\n : operator.startsWith(\"IS \")\n ? withoutNullTest(corrected, field)\n : asListCondition(corrected, field),\n query,\n );\n return {\n ok: false,\n error: {\n code: \"INVALID_OPERATOR\",\n message: `${first.operator} cannot be used on ${first.field}: it is a list field, so Google would refuse this query.`,\n fix: {\n action: \"change_operator\",\n ...(fixed ? { correctedCommand: buildCommand(fixed, customerId) } : {}),\n explanation: listFieldGuidance(first.field),\n },\n retryable: false,\n },\n };\n}\n\n/** REJECT the function form CONTAINS(field, 'x'), which GAQL does not have — LIKE is the substring match. */\nfunction rejectContainsCall(query: string, customerId: string): AdsErrorEnvelope | null {\n const containsMatch = query.match(/CONTAINS\\s*\\(\\s*([^,]+),\\s*'([^']+)'\\s*\\)/i);\n if (!containsMatch) return null;\n const field = containsMatch[1]?.trim();\n const value = containsMatch[2] ?? \"\";\n const fixed = query.replace(/CONTAINS\\s*\\(\\s*[^,]+,\\s*'[^']+'\\s*\\)/i, `${field} LIKE '%${value}%'`);\n return {\n ok: false,\n error: {\n code: \"INVALID_OPERATOR\",\n message: \"GAQL does not support CONTAINS. Use LIKE with % wildcards.\",\n fix: {\n action: \"change_operator\",\n correctedCommand: buildCommand(fixed, customerId),\n explanation: \"GAQL uses LIKE '%value%' for substring matching, not CONTAINS()\",\n },\n retryable: false,\n },\n };\n}\n\nexport function validatePreflight(query: string, customerId: string): PreflightResult {\n const warnings: AdsWarning[] = [];\n const corrected = query;\n\n // REJECT: Non-SELECT queries\n const firstWord = corrected.trim().split(/\\s+/)[0]?.toUpperCase();\n if (firstWord && firstWord !== \"SELECT\") {\n return {\n valid: false,\n warnings: [],\n error: {\n ok: false,\n error: {\n code: \"READ_ONLY\",\n message: `Only SELECT queries are allowed. Got: ${firstWord}`,\n fix: {\n action: \"reject\",\n explanation: \"This CLI only supports GAQL SELECT queries for reading data\",\n },\n retryable: false,\n },\n },\n };\n }\n\n // REJECT: Open-ended date range\n const openDateMatch = corrected.match(/segments\\.date\\s*>=\\s*'(\\d{4}-\\d{2}-\\d{2})'/i);\n if (openDateMatch && !/BETWEEN/i.test(corrected)) {\n const startDate = openDateMatch[1] ?? \"\";\n const today = new Date().toISOString().slice(0, 10);\n const fixed = corrected.replace(\n /segments\\.date\\s*>=\\s*'(\\d{4}-\\d{2}-\\d{2})'/i,\n `segments.date BETWEEN '${startDate}' AND '${today}'`,\n );\n return {\n valid: false,\n warnings: [],\n error: {\n ok: false,\n error: {\n code: \"OPEN_ENDED_DATE\",\n message: \"Open-ended date ranges are not supported. Use BETWEEN or DURING.\",\n fix: {\n action: \"narrow_date_range\",\n correctedCommand: buildCommand(fixed, customerId),\n explanation: \"Use BETWEEN with explicit start and end dates\",\n },\n retryable: false,\n },\n },\n };\n }\n\n for (const reject of [rejectContainsCall, rejectListFieldOperator, rejectUnsupportedDuring]) {\n const rejection = reject(corrected, customerId);\n if (rejection) return { valid: false, warnings: [], error: rejection };\n }\n\n // REJECT: conversion action segments are incompatible with cost metrics in GAQL.\n // Split conversion-action breakdowns from spend/cost queries and join by campaign in report code.\n if (/\\bsegments\\.conversion_action_name\\b/i.test(corrected) && /\\bmetrics\\.cost_micros\\b/i.test(corrected)) {\n return {\n valid: false,\n warnings: [],\n error: {\n ok: false,\n error: {\n code: \"INCOMPATIBLE_FIELDS\",\n message: \"segments.conversion_action_name cannot be selected with metrics.cost_micros in one GAQL query.\",\n fix: {\n action: \"split_query\",\n explanation:\n \"Run one campaign spend query with metrics.cost_micros and a separate conversion-action query without cost metrics, then join the results by campaign.\",\n },\n retryable: false,\n },\n },\n };\n }\n\n // WARN: ad_group_criterion without negative field — positives and negatives will be mixed\n if (/FROM\\s+ad_group_criterion\\b/i.test(corrected) && !/ad_group_criterion\\.negative/i.test(corrected)) {\n warnings.push({\n code: \"NEGATIVE_FIELD_MISSING\",\n message:\n \"ad_group_criterion returns BOTH positive and negative keywords. Add ad_group_criterion.negative to SELECT or filter with WHERE ad_group_criterion.negative = FALSE (positives) / TRUE (negatives). Use --preset positive-keywords or negative-keywords instead.\",\n });\n }\n\n const { query: autoFixed, warnings: autoFixWarnings } = applyAutoFixes(corrected);\n warnings.push(...autoFixWarnings);\n\n // Runs last, on the auto-fixed query, so ACTIVE → ENABLED is already normalised into the chain.\n const analysis = analyzeServingChain(autoFixed);\n if (!analysis) return { valid: true, correctedQuery: autoFixed, warnings };\n\n const rejection = rejectPartialChain(autoFixed, customerId, analysis);\n if (rejection) return { valid: false, warnings: [], error: rejection };\n\n const annotated = annotateUnscopedChain(autoFixed, analysis);\n warnings.push(...annotated.warnings);\n\n return { valid: true, correctedQuery: annotated.query, warnings };\n}\n","/**\n * Refusing to let one `metrics.conversions` column stand for several different events.\n *\n * `metrics.conversions` is not a metric, it is a sum: each campaign contributes whatever conversion\n * actions the account has marked primary for it, and in a real account those differ campaign by\n * campaign — a webinar registration here, a form fill there, a CRM-stage SQL somewhere else. So a\n * total across campaigns adds events that are not the same event, and a conversion rate from one\n * campaign is not comparable with another's.\n *\n * This was learned on a report that put \"our landings 4,6 % CVR vs theirs 3,2 %\" in front of the\n * client. The two sides were different campaign types counting different actions; the client caught\n * it, and the comparison had to be withdrawn. The read that produced it looked completely ordinary:\n * one `landing_page_view` pull with `metrics.conversions` and no campaign filter.\n *\n * The check is deliberately on the statement, not on the rows: the `--out` path streams pages to a\n * file and keeps none of them, and that is exactly the shape a big blended pull takes.\n */\n\nimport type { AdsWarning } from \"../types.ts\";\nimport { isSingleRowRead } from \"./preflight.ts\";\n\n/** Metrics whose value is a sum, or a rate, over whichever conversion actions each campaign counts. */\nconst CONVERSION_METRICS = [\n \"METRICS.CONVERSIONS\",\n \"METRICS.ALL_CONVERSIONS\",\n \"METRICS.CONVERSIONS_VALUE\",\n \"METRICS.ALL_CONVERSIONS_VALUE\",\n \"METRICS.COST_PER_CONVERSION\",\n \"METRICS.COST_PER_ALL_CONVERSIONS\",\n \"METRICS.CONVERSIONS_FROM_INTERACTIONS_RATE\",\n \"METRICS.ALL_CONVERSIONS_FROM_INTERACTIONS_RATE\",\n \"METRICS.VALUE_PER_CONVERSION\",\n \"METRICS.VALUE_PER_ALL_CONVERSIONS\",\n];\n\n/** Segments that split the metric back into one row per action, which is what makes it readable. */\nconst CONVERSION_SEGMENTS = [\"SEGMENTS.CONVERSION_ACTION_NAME\", \"SEGMENTS.CONVERSION_ACTION=\"];\n\n/** A campaign identity pinned by an equality predicate — the one shape that confines a read. */\nconst CAMPAIGN_EQUALITY = /\\bCAMPAIGN\\.(ID|NAME|RESOURCE_NAME)\\s*=/;\n\n// Everything between WHERE and the first trailing clause. A campaign column in the SELECT list\n// names a dimension the rows are broken down by; only a predicate confines the read to one.\nfunction whereClause(upperQuery: string): string {\n const whereIdx = upperQuery.indexOf(\" WHERE \");\n if (whereIdx === -1) return \"\";\n let rest = upperQuery.slice(whereIdx + 7);\n for (const trailing of [\" ORDER BY \", \" GROUP BY \", \" LIMIT \", \" PARAMETERS \"]) {\n const idx = rest.indexOf(trailing);\n if (idx !== -1) rest = rest.slice(0, idx);\n }\n return rest;\n}\n\nexport function conversionMixWarnings(finalQuery: string): AdsWarning[] {\n const upper = ` ${finalQuery.toUpperCase().replace(/\\s+/g, \" \")} `;\n const readsConversions = CONVERSION_METRICS.some((metric) => upper.includes(metric));\n if (!readsConversions) return [];\n if (CONVERSION_SEGMENTS.some((segment) => upper.includes(segment))) return [];\n if (CAMPAIGN_EQUALITY.test(whereClause(upper))) return [];\n // One account-wide row carries no campaign to total across or set beside another.\n if (isSingleRowRead(finalQuery)) return [];\n\n return [\n {\n code: \"CONVERSION_MIX\",\n message:\n \"MIXED CONVERSION ACTIONS — this read covers more than one campaign, and a campaign's conversion figures count whatever conversion actions are primary for it. Those differ campaign by campaign (a webinar registration, an info request, a CRM-stage SQL), so a total across these rows adds different events and a conversion rate from one campaign is not comparable with another's. Before totalling or comparing, re-run segmented by segments.conversion_action_name and check the campaigns you are putting side by side count the same action. If they do not, compare only within one action — and carry that caveat into anything you write for the user, not just the analysis.\",\n },\n ];\n}\n","import type { AdsWarning } from \"../types.ts\";\n\n// Google Ads' UI renders \"Eligible (Limited) — Below first page bid (€3.63)\" from three separate\n// API fields: ad_group_criterion.primary_status_reasons, position_estimates.first_page_cpc_micros\n// and effective_cpc_bid_micros. Read raw, that is an enum name and two micro-integers — this\n// module turns them back into the sentence the column was showing, plus what fixes it.\n\nconst BELOW_FIRST_PAGE_BID = \"AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID\";\n\n// Reasons the keyword itself can act on. The parent-chain ones (CAMPAIGN_PAUSED, AD_GROUP_PAUSED,\n// AD_GROUP_CRITERION_PAUSED/REMOVED, …) restate a status the row already carries, so they are left\n// out rather than reported as a finding.\nconst ACTIONABLE_REASONS: Record<string, { label: string; advice: string }> = {\n [BELOW_FIRST_PAGE_BID]: {\n label: \"Below first page bid\",\n advice:\n \"raise the max CPC (keywords update --cpc-bid) to at least the first-page estimate, or move the ad group onto a smart bidding strategy that sets the bid itself\",\n },\n AD_GROUP_CRITERION_LOW_QUALITY: {\n label: \"Low quality score\",\n advice:\n \"a bid raise does not lift quality — tighten ad relevance to the keyword and the landing page it points at (ad_group_criterion.quality_info.* breaks the score into its three parts)\",\n },\n AD_GROUP_CRITERION_RARELY_SERVED: {\n label: \"Rarely served\",\n advice: \"too few people search this term — broaden the match type or drop it, don't bid it up\",\n },\n AD_GROUP_CRITERION_DISAPPROVED: {\n label: \"Disapproved\",\n advice: \"select ad_group_criterion.disapproval_reasons to see which policy blocked it\",\n },\n AD_GROUP_CRITERION_RESTRICTED: {\n label: \"Restricted\",\n advice: \"policy limits where and to whom this keyword may show\",\n },\n AD_GROUP_CRITERION_UNDER_REVIEW: {\n label: \"Under review\",\n advice: \"waiting on Google's review — nothing to change yet\",\n },\n AD_GROUP_CRITERION_PENDING_REVIEW: {\n label: \"Pending review\",\n advice: \"waiting on Google's review — nothing to change yet\",\n },\n AD_GROUP_CRITERION_PAUSED_DUE_TO_LOW_ACTIVITY: {\n label: \"Auto-paused for low activity\",\n advice: \"Google paused it after prolonged low serving — resume it only alongside a change that gives it traffic\",\n },\n};\n\nconst REASON_ORDER = Object.keys(ACTIONABLE_REASONS);\nconst MAX_NAMED_KEYWORDS = 5;\n\n// Google's REST transport serialises int64 as a JSON string, so a micros field arrives as\n// \"3630000\" from the API and as 3630000 from a hand-built fixture. Both have to read the same.\nfunction toMicros(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\" && value.trim() !== \"\" && Number.isFinite(Number(value))) return Number(value);\n return null;\n}\n\nfunction formatMoney(micros: number): string {\n return (micros / 1_000_000).toFixed(2);\n}\n\n// A keyword's criterion id is derived from its text + match type, so the same id and the same text\n// repeat in every ad group holding that keyword. Naming a copy without its ad group makes the\n// copies indistinguishable — and identical enough to be silently deduped out of the count.\nfunction keywordLabel(row: Record<string, unknown>): string {\n const text = row[\"ad_group_criterion.keyword.text\"];\n const id = row[\"ad_group_criterion.criterion_id\"];\n const name = typeof text === \"string\" && text !== \"\" ? text : id == null ? \"(unnamed keyword)\" : `criterion ${id}`;\n const adGroup = row[\"ad_group.name\"];\n return typeof adGroup === \"string\" && adGroup !== \"\" ? `${name} (ad group ${adGroup})` : name;\n}\n\n// What makes two rows the same keyword for counting. The ad group is half the identity, so it is\n// part of the key whenever the read selected it.\nfunction keywordKey(row: Record<string, unknown>): string {\n const adGroup = row[\"ad_group.id\"] ?? row[\"ad_group.name\"] ?? \"\";\n const criterion = row[\"ad_group_criterion.criterion_id\"] ?? row[\"ad_group_criterion.keyword.text\"] ?? \"\";\n return `${String(adGroup)}~${String(criterion)}` === \"~\" ? keywordLabel(row) : `${adGroup}~${criterion}`;\n}\n\nfunction reasonsOn(row: Record<string, unknown>): string[] | null {\n const raw = row[\"ad_group_criterion.primary_status_reasons\"];\n if (!Array.isArray(raw)) return null;\n return raw.filter((r): r is string => typeof r === \"string\");\n}\n\n// The bid gap the API states outright, for reads that selected the estimates without the status.\nfunction bidShortfall(row: Record<string, unknown>): string | null {\n const bid = toMicros(row[\"ad_group_criterion.effective_cpc_bid_micros\"] ?? row[\"ad_group_criterion.cpc_bid_micros\"]);\n const firstPage = toMicros(row[\"ad_group_criterion.position_estimates.first_page_cpc_micros\"]);\n if (bid == null || firstPage == null || bid >= firstPage) return null;\n return `bids ${formatMoney(bid)} against a ${formatMoney(firstPage)} first-page estimate`;\n}\n\n// The limits one row reports, each already phrased with the keyword it belongs to.\n//\n// Google omits primary_status_reasons entirely when a keyword has none, so an absent key means\n// two different things: \"nothing to report\" if the read selected the field, \"we never asked\" if it\n// didn't. Only the query knows which, hence statusReasonsSelected — without it, every healthy\n// keyword bidding under a stale estimate gets reported as starved.\nfunction limitsOn(\n row: Record<string, unknown>,\n options: KeywordServingOptions,\n): Array<{ reason: string; detail: string }> {\n const reasons = reasonsOn(row);\n const shortfall = bidShortfall(row);\n const label = keywordLabel(row);\n\n const applicable = reasons\n ? reasons.filter((reason) => reason in ACTIONABLE_REASONS)\n : options.statusReasonsSelected || shortfall === null\n ? []\n : [BELOW_FIRST_PAGE_BID];\n\n return applicable.map((reason) => ({\n reason,\n detail: reason === BELOW_FIRST_PAGE_BID && shortfall ? `\"${label}\" ${shortfall}` : `\"${label}\"`,\n }));\n}\n\nfunction describeLimit(reason: string, details: string[]): string {\n const named = details.slice(0, MAX_NAMED_KEYWORDS).join(\"; \");\n const rest = details.length > MAX_NAMED_KEYWORDS ? ` (+${details.length - MAX_NAMED_KEYWORDS} more)` : \"\";\n const { label, advice } = ACTIONABLE_REASONS[reason] as { label: string; advice: string };\n return `${label} — ${details.length}: ${named}${rest}. Fix: ${advice}.`;\n}\n\nexport interface KeywordServingOptions {\n /** Whether the executed query selected ad_group_criterion.primary_status_reasons. */\n statusReasonsSelected: boolean;\n}\n\n/** Limits found so far: the details to quote per reason, and which keywords they belong to. */\nexport interface KeywordLimits {\n detailsByReason: Map<string, string[]>;\n keywords: Set<string>;\n}\n\n/**\n * Fold one page of rows into the running limits. `--out` streams each page to disk and keeps no\n * rows, so the tally has to survive between pages rather than be recomputed at the end.\n */\nexport function collectKeywordLimits(\n rows: Array<Record<string, unknown>>,\n options: KeywordServingOptions,\n acc: KeywordLimits = { detailsByReason: new Map(), keywords: new Set() },\n): KeywordLimits {\n for (const row of rows) {\n const limits = limitsOn(row, options);\n if (limits.length === 0) continue;\n // Counted once per keyword, however many reasons it carries — Google reports below-first-page-\n // bid and low-quality together on the same keyword all the time.\n acc.keywords.add(keywordKey(row));\n for (const { reason, detail } of limits) {\n const bucket = acc.detailsByReason.get(reason);\n if (bucket) bucket.push(detail);\n else acc.detailsByReason.set(reason, [detail]);\n }\n }\n return acc;\n}\n\n/** Render accumulated limits as the single warning the agent reads. Empty when nothing is limited. */\nexport function renderKeywordLimits(limits: KeywordLimits): AdsWarning[] {\n if (limits.detailsByReason.size === 0) return [];\n\n const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map((reason) =>\n describeLimit(reason, limits.detailsByReason.get(reason) ?? []),\n );\n const count = limits.keywords.size;\n\n return [\n {\n code: \"KEYWORD_SERVING_LIMITED\",\n message: `${count} ${count === 1 ? \"keyword is\" : \"keywords are\"} limited and not getting the traffic they could. ${sentences.join(\" \")} Amounts are in the account currency (baker ads google currency).`,\n },\n ];\n}\n\n/**\n * Turn the serving signals on a keyword read into one warning naming what is limited and what\n * fixes it. Silent when the rows carry no serving signal, or when every limit is just a paused\n * parent — a row that already says PAUSED needs no second telling.\n */\nexport function keywordServingWarnings(\n rows: Array<Record<string, unknown>>,\n options: KeywordServingOptions,\n): AdsWarning[] {\n return renderKeywordLimits(collectKeywordLimits(rows, options));\n}\n","/**\n * Google Ads reads keep money in micros (millionths of the account currency),\n * exactly as GAQL returns it, while every money flag (`--amount`, `--cpc-bid`,\n * `--target-cpa`, …) takes whole currency. An amount copied from one into the\n * other is a millionfold raise, so a read that carries a budget, bid or target\n * says so once, next to the rows.\n *\n * Only the settings a flag is set from count: cost metrics are read, not copied\n * into a write, and saying it on every performance read would teach the agent to\n * skip warnings. `*_fraction_micros` is a share, not money.\n */\n\nimport type { AdsWarning } from \"../types.ts\";\n\nfunction isSettingMicros(field: string): boolean {\n return field.endsWith(\"_micros\") && !field.startsWith(\"metrics.\") && !field.endsWith(\"_fraction_micros\");\n}\n\nexport function moneyUnitWarnings(fields: readonly string[]): AdsWarning[] {\n const example = fields.find(isSettingMicros);\n if (!example) return [];\n return [\n {\n code: \"MONEY_IN_MICROS\",\n message:\n `Budgets and bids in these rows are in micros, millionths of the account currency: ${example} 20000000 is 20. ` +\n \"Write flags (--amount, --cpc-bid, --max-cpc, --target-cpa, --target-cpc) take whole currency: --amount 20, never the micros.\",\n },\n ];\n}\n"],"mappings":"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","names":["literal","rejection","whereClause","whereClause"]}
|