@koda-sl/baker-cli 0.244.0 → 0.246.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/cli.js CHANGED
@@ -9408,8 +9408,8 @@ function countStaccato(text2) {
9408
9408
  let first = "";
9409
9409
  let runStart = 0;
9410
9410
  for (const [i, s] of sentences.entries()) {
9411
- const words3 = s.split(/\s+/).filter(Boolean).length;
9412
- if (words3 > 0 && words3 <= STACCATO_MAX_WORDS) {
9411
+ const words2 = s.split(/\s+/).filter(Boolean).length;
9412
+ if (words2 > 0 && words2 <= STACCATO_MAX_WORDS) {
9413
9413
  if (run === 0) runStart = i;
9414
9414
  run++;
9415
9415
  if (run === STACCATO_RUN) {
@@ -9427,10 +9427,10 @@ function countTitleCaseHeadings(text2) {
9427
9427
  let first = "";
9428
9428
  for (const m of text2.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
9429
9429
  const heading = (m[1] ?? "").trim();
9430
- const words3 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
9431
- if (words3.length < 4) continue;
9432
- const capitalized = words3.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
9433
- if (capitalized / words3.length < 0.8) continue;
9430
+ const words2 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
9431
+ if (words2.length < 4) continue;
9432
+ const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
9433
+ if (capitalized / words2.length < 0.8) continue;
9434
9434
  count++;
9435
9435
  if (!first) first = heading;
9436
9436
  }
@@ -9679,7 +9679,7 @@ function renderAdvisories(data, lines) {
9679
9679
  return;
9680
9680
  }
9681
9681
  lines.push("");
9682
- lines.push("Advisories (non-blocking \u2014 build these out for a higher-performance campaign)");
9682
+ lines.push("Advisories (non-blocking, checked across the whole draft \u2014 settle these before you publish)");
9683
9683
  for (const advisory of advisories) {
9684
9684
  lines.push(` \u2022 ${advisory.message}`);
9685
9685
  }
@@ -9779,11 +9779,11 @@ function rawTextEntries(value) {
9779
9779
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
9780
9780
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
9781
9781
  }
9782
- function rawFileEntries(path40) {
9783
- if (typeof path40 !== "string" || path40.length === 0) {
9782
+ function rawFileEntries(path38) {
9783
+ if (typeof path38 !== "string" || path38.length === 0) {
9784
9784
  return [];
9785
9785
  }
9786
- return readFileSync2(path40, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9786
+ return readFileSync2(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9787
9787
  }
9788
9788
  function keywordEntries(args) {
9789
9789
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -9806,19 +9806,19 @@ function keywordEntries(args) {
9806
9806
  }
9807
9807
  return entries;
9808
9808
  }
9809
- function loadJsonFileArg(path40) {
9810
- if (typeof path40 !== "string" || path40.length === 0) {
9809
+ function loadJsonFileArg(path38) {
9810
+ if (typeof path38 !== "string" || path38.length === 0) {
9811
9811
  return {};
9812
9812
  }
9813
9813
  try {
9814
- const parsed = JSON.parse(readFileSync2(path40, "utf8"));
9814
+ const parsed = JSON.parse(readFileSync2(path38, "utf8"));
9815
9815
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9816
- failWriteValidation(`${path40} must contain a JSON object`);
9816
+ failWriteValidation(`${path38} must contain a JSON object`);
9817
9817
  }
9818
9818
  return parsed;
9819
9819
  } catch (err) {
9820
9820
  if (err instanceof SyntaxError) {
9821
- failWriteValidation(`${path40} is not valid JSON: ${err.message}`);
9821
+ failWriteValidation(`${path38} is not valid JSON: ${err.message}`);
9822
9822
  }
9823
9823
  throw err;
9824
9824
  }
@@ -9948,10 +9948,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
9948
9948
  async function stageTarget(kind, customerId, target, hints) {
9949
9949
  await stageGoogleOp({ kind, customerId, target }, hints);
9950
9950
  }
9951
- async function draftAction(path40, body, chat) {
9951
+ async function draftAction(path38, body, chat) {
9952
9952
  try {
9953
9953
  const chatId = resolveChatId(chat);
9954
- const response = await apiPost(path40, { chatId, ...body });
9954
+ const response = await apiPost(path38, { chatId, ...body });
9955
9955
  writeJsonEnvelope(response);
9956
9956
  } catch (err) {
9957
9957
  handleGoogleError(err);
@@ -10188,10 +10188,33 @@ function warnDefaults(ctx) {
10188
10188
  }
10189
10189
  }
10190
10190
 
10191
+ // src/commands/ads/google/keywords/provenance.ts
10192
+ var PLANNER_SOURCE = "keyword_planner_estimate";
10193
+ var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["keyword"]);
10194
+ function stampPlannerRows(rows) {
10195
+ return rows.map((row) => {
10196
+ const stamped = { source: PLANNER_SOURCE };
10197
+ for (const [key, value] of Object.entries(row)) {
10198
+ stamped[IDENTITY_FIELDS.has(key) || key.startsWith("planner_") ? key : `planner_${key}`] = value;
10199
+ }
10200
+ return stamped;
10201
+ });
10202
+ }
10203
+ var PLANNER_FIELD_DESCRIPTIONS = {
10204
+ source: `Always "${PLANNER_SOURCE}". These rows are Keyword Planner ESTIMATES of total market search volume, NOT this account's data. Never report them as impressions, clicks or any account metric \u2014 read those from a query against the account.`,
10205
+ keyword: "The keyword this estimate is for",
10206
+ planner_avg_monthly_searches: "ESTIMATE: average monthly searches across the whole market (string), last 12 months",
10207
+ planner_competition: "ESTIMATE: competition level \u2014 LOW, MEDIUM, HIGH",
10208
+ planner_competition_index: "ESTIMATE: competition index 0-100 as string (higher = more competitive)",
10209
+ planner_low_top_of_page_bid_micros: "ESTIMATE: low-range top-of-page CPC bid in micros (\xF7 1,000,000 for currency)",
10210
+ planner_high_top_of_page_bid_micros: "ESTIMATE: high-range top-of-page CPC bid in micros (\xF7 1,000,000 for currency)",
10211
+ planner_monthly_search_volumes: "ESTIMATE: monthly market search volume breakdown (array of {year, month, monthly_searches})"
10212
+ };
10213
+
10191
10214
  // src/commands/ads/google/keywords/discover.ts
10192
10215
  registerSchema({
10193
10216
  command: "ads.google.keywords.discover",
10194
- description: "Discover keyword ideas from seed keywords or URLs. Returns { keywords, total_results, next_page_token? } with snake_case fields matching the Google Ads API. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.",
10217
+ description: 'Discover keyword ideas from seed keywords or URLs. Returns { source: "keyword_planner_estimate", keywords, total_results, next_page_token? }. THESE ARE ESTIMATES OF TOTAL MARKET SEARCH VOLUME, NOT THIS ACCOUNT\'S DATA: every row carries a source column and every metric is prefixed planner_ (planner_avg_monthly_searches, planner_competition, ...). Never report a planner_ number as impressions, clicks or any account metric \u2014 read those with `baker ads google query` against the account. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.',
10195
10218
  args: {
10196
10219
  "customer-id": {
10197
10220
  type: "string",
@@ -10220,15 +10243,9 @@ registerSchema({
10220
10243
  "no-cache": { type: "boolean", description: "Skip cache", required: false }
10221
10244
  }
10222
10245
  });
10223
- var KEYWORD_IDEA_FIELDS = {
10224
- keyword: "Suggested keyword text",
10225
- avg_monthly_searches: "Average monthly search volume (string)",
10226
- competition: "Competition level: LOW, MEDIUM, HIGH",
10227
- competition_index: "Competition index 0-100 as string (higher = more competitive)",
10228
- low_top_of_page_bid_micros: "Low-range CPC bid in micros (\xF7 1,000,000 for currency)",
10229
- high_top_of_page_bid_micros: "High-range CPC bid in micros (\xF7 1,000,000 for currency)",
10230
- monthly_search_volumes: "Monthly search volume breakdown (array of {year, month, monthly_searches})"
10231
- };
10246
+ function stampDiscoverResponse(data) {
10247
+ return { ...data, source: PLANNER_SOURCE, keywords: stampPlannerRows(data.keywords ?? []) };
10248
+ }
10232
10249
  function buildDiscoverBody(customerId, args) {
10233
10250
  const body = {
10234
10251
  customerId,
@@ -10269,6 +10286,8 @@ var discoverCommand = defineCommand23({
10269
10286
  name: "discover",
10270
10287
  description: `Discover new keyword ideas from seed keywords or competitor URLs.
10271
10288
 
10289
+ Returns Keyword Planner ESTIMATES of market search volume, not this account's numbers. Every row carries source=keyword_planner_estimate and planner_-prefixed metrics; keep those names when you quote a figure, and never present one as an impression or click count.
10290
+
10272
10291
  Examples:
10273
10292
  baker ads google keywords discover --customer-id 1234567890 --seeds "running shoes,athletic footwear"
10274
10293
  baker ads google keywords discover --customer-id 1234567890 --url "https://competitor.com"
@@ -10303,7 +10322,7 @@ Examples:
10303
10322
  const cached = cacheGet("keywords", cacheKey);
10304
10323
  if (cached) {
10305
10324
  warnDefaults(queryContext);
10306
- writeAdsJson({ ok: true, data: cached.data, cached: true, query_context: queryContext });
10325
+ writeAdsJson({ ok: true, data: stampDiscoverResponse(cached.data), cached: true, query_context: queryContext });
10307
10326
  return;
10308
10327
  }
10309
10328
  }
@@ -10313,12 +10332,13 @@ Examples:
10313
10332
  cacheSet("keywords", cacheKey, data, 24 * 60 * 60 * 1e3);
10314
10333
  }
10315
10334
  warnDefaults(queryContext);
10335
+ const stamped = stampDiscoverResponse(data);
10316
10336
  const format = args.output || "json";
10317
10337
  if (format !== "json") {
10318
- writeAdsOutput(data.keywords, format);
10338
+ writeAdsOutput(stamped.keywords, format);
10319
10339
  return;
10320
10340
  }
10321
- writeAdsJson({ ok: true, data, fields: KEYWORD_IDEA_FIELDS, query_context: queryContext });
10341
+ writeAdsJson({ ok: true, data: stamped, fields: PLANNER_FIELD_DESCRIPTIONS, query_context: queryContext });
10322
10342
  } catch (err) {
10323
10343
  handleKeywordError(err);
10324
10344
  }
@@ -10371,7 +10391,7 @@ var locationsCommand = defineCommand25({
10371
10391
  import { defineCommand as defineCommand26 } from "citty";
10372
10392
  registerSchema({
10373
10393
  command: "ads.google.keywords.metrics",
10374
- description: "Get historical metrics for specific keywords. Returns { historical_metrics: [...] } with snake_case fields matching the Google Ads API. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.",
10394
+ description: 'Get historical market metrics for specific keywords. Returns { source: "keyword_planner_estimate", historical_metrics: [...] }. THESE ARE KEYWORD PLANNER ESTIMATES OF TOTAL MARKET SEARCH VOLUME, NOT THIS ACCOUNT\'S DATA: every row carries a source column and every metric is prefixed planner_ (planner_avg_monthly_searches, planner_competition, ...). Never report a planner_ number as impressions, clicks or any account metric \u2014 read those with `baker ads google query` against the account. IMPORTANT: If --location and --language are omitted, defaults to United States (2840) and English (1000). The response includes a query_context object showing which location/language were used.',
10375
10395
  args: {
10376
10396
  "customer-id": {
10377
10397
  type: "string",
@@ -10393,6 +10413,9 @@ registerSchema({
10393
10413
  "no-cache": { type: "boolean", description: "Skip cache", required: false }
10394
10414
  }
10395
10415
  });
10416
+ function stampMetricsResponse(data) {
10417
+ return { ...data, source: PLANNER_SOURCE, historical_metrics: stampPlannerRows(data.historical_metrics ?? []) };
10418
+ }
10396
10419
  function handleMetricsError(err) {
10397
10420
  if (err instanceof ApiError) {
10398
10421
  if (isNotConnectedError(err.code, err.message)) {
@@ -10418,6 +10441,8 @@ var metricsCommand = defineCommand26({
10418
10441
  name: "metrics",
10419
10442
  description: `Get historical search metrics for specific keywords.
10420
10443
 
10444
+ Returns Keyword Planner ESTIMATES of market search volume, not this account's numbers. Every row carries source=keyword_planner_estimate and planner_-prefixed metrics; keep those names when you quote a figure, and never present one as an impression or click count.
10445
+
10421
10446
  Examples:
10422
10447
  baker ads google keywords metrics --customer-id 1234567890 --keywords "running shoes,nike shoes,adidas shoes"
10423
10448
  baker ads google keywords metrics --customer-id 1234567890 --keywords "seo tools" --location 2826`
@@ -10454,7 +10479,7 @@ Examples:
10454
10479
  const cached = cacheGet("keywords", cacheKey);
10455
10480
  if (cached) {
10456
10481
  warnDefaults(queryContext);
10457
- writeAdsJson({ ok: true, data: cached.data, cached: true, query_context: queryContext });
10482
+ writeAdsJson({ ok: true, data: stampMetricsResponse(cached.data), cached: true, query_context: queryContext });
10458
10483
  return;
10459
10484
  }
10460
10485
  }
@@ -10464,21 +10489,13 @@ Examples:
10464
10489
  cacheSet("keywords", cacheKey, data, 24 * 60 * 60 * 1e3);
10465
10490
  }
10466
10491
  warnDefaults(queryContext);
10492
+ const stamped = stampMetricsResponse(data);
10467
10493
  const format = args.output || "json";
10468
10494
  if (format !== "json") {
10469
- writeAdsOutput(data.historical_metrics, format);
10495
+ writeAdsOutput(stamped.historical_metrics, format);
10470
10496
  return;
10471
10497
  }
10472
- const fields = {
10473
- keyword: "The keyword analyzed",
10474
- avg_monthly_searches: "Average monthly search volume as string (last 12 months)",
10475
- competition: "Competition level: LOW, MEDIUM, HIGH",
10476
- competition_index: "Competition index 0-100 as string (higher = more competitive)",
10477
- low_top_of_page_bid_micros: "Low-range CPC bid in micros (\xF7 1,000,000 for currency)",
10478
- high_top_of_page_bid_micros: "High-range CPC bid in micros (\xF7 1,000,000 for currency)",
10479
- monthly_search_volumes: "Monthly search volume breakdown (array of {year, month, monthly_searches})"
10480
- };
10481
- writeAdsJson({ ok: true, data, fields, query_context: queryContext });
10498
+ writeAdsJson({ ok: true, data: stamped, fields: PLANNER_FIELD_DESCRIPTIONS, query_context: queryContext });
10482
10499
  } catch (err) {
10483
10500
  handleMetricsError(err);
10484
10501
  }
@@ -10625,10 +10642,211 @@ function keywordServingWarnings(rows, options) {
10625
10642
  return renderKeywordLimits(collectKeywordLimits(rows, options));
10626
10643
  }
10627
10644
 
10645
+ // src/commands/ads/google/presets.ts
10646
+ var GAQL_DATE_RANGE_RE = /^(?:TODAY|YESTERDAY|LAST_7_DAYS|LAST_14_DAYS|LAST_30_DAYS|LAST_90_DAYS|THIS_MONTH|LAST_MONTH|THIS_QUARTER|LAST_QUARTER|THIS_YEAR|LAST_YEAR|ALL_TIME|BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}')$/i;
10647
+ function isValidDateRange(value) {
10648
+ return GAQL_DATE_RANGE_RE.test(value.trim());
10649
+ }
10650
+ var SERVING_CHAINS = {
10651
+ keyword_view: ["campaign.status", "ad_group.status", "ad_group_criterion.status"],
10652
+ ad_group_criterion: ["campaign.status", "ad_group.status", "ad_group_criterion.status"],
10653
+ ad_group_ad: ["campaign.status", "ad_group.status", "ad_group_ad.status"],
10654
+ search_term_view: ["campaign.status", "ad_group.status"],
10655
+ ad_group: ["campaign.status", "ad_group.status"],
10656
+ asset_group: ["campaign.status", "asset_group.status"],
10657
+ asset_group_asset: ["campaign.status", "asset_group.status"]
10658
+ };
10659
+ var PRESETS = [
10660
+ {
10661
+ name: "campaign-performance",
10662
+ description: "Campaign-level metrics overview",
10663
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
10664
+ defaultDateRange: "LAST_30_DAYS",
10665
+ defaultLimit: 200,
10666
+ statusFields: ["campaign.status"]
10667
+ },
10668
+ {
10669
+ name: "keyword-analysis",
10670
+ description: "Keyword performance with match type and quality",
10671
+ 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, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10672
+ defaultDateRange: "LAST_30_DAYS",
10673
+ defaultLimit: 200,
10674
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10675
+ },
10676
+ {
10677
+ name: "keyword-serving",
10678
+ description: "Why a keyword is limited \u2014 Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score",
10679
+ 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 LIMIT {limit}`,
10680
+ defaultDateRange: "ALL_TIME",
10681
+ defaultLimit: 500,
10682
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10683
+ },
10684
+ {
10685
+ name: "positive-keywords",
10686
+ description: "Positive (targeting) keywords only \u2014 excludes negatives",
10687
+ 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 LIMIT {limit}`,
10688
+ defaultDateRange: "ALL_TIME",
10689
+ defaultLimit: 500,
10690
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10691
+ },
10692
+ {
10693
+ name: "negative-keywords",
10694
+ description: "Negative keywords only \u2014 campaign and ad group level",
10695
+ 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 LIMIT {limit}`,
10696
+ defaultDateRange: "ALL_TIME",
10697
+ defaultLimit: 500
10698
+ },
10699
+ {
10700
+ name: "negative-keyword-lists",
10701
+ description: "Shared negative keyword lists and the terms inside them (SharedSet members)",
10702
+ gaqlTemplate: `SELECT shared_set.id, shared_set.name, 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 LIMIT {limit}`,
10703
+ defaultDateRange: "ALL_TIME",
10704
+ defaultLimit: 500
10705
+ },
10706
+ {
10707
+ name: "negative-list-attachments",
10708
+ description: "Which campaigns each shared negative keyword list is attached to",
10709
+ 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 LIMIT {limit}`,
10710
+ defaultDateRange: "ALL_TIME",
10711
+ defaultLimit: 500
10712
+ },
10713
+ {
10714
+ name: "search-terms",
10715
+ description: "Actual user search queries triggering ads",
10716
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10717
+ defaultDateRange: "LAST_7_DAYS",
10718
+ defaultLimit: 200,
10719
+ statusFields: ["campaign.status", "ad_group.status"]
10720
+ },
10721
+ {
10722
+ name: "ad-copy-performance",
10723
+ description: "Ad headline and description effectiveness",
10724
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.status, 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.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10725
+ defaultDateRange: "LAST_30_DAYS",
10726
+ defaultLimit: 200,
10727
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_ad.status"]
10728
+ },
10729
+ {
10730
+ name: "asset-performance",
10731
+ description: "Performance Max asset performance labels",
10732
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} LIMIT {limit}`,
10733
+ defaultDateRange: "LAST_30_DAYS",
10734
+ defaultLimit: 200,
10735
+ statusFields: ["campaign.status", "asset_group.status"]
10736
+ },
10737
+ {
10738
+ name: "shopping-products",
10739
+ description: "Product-level shopping performance metrics",
10740
+ 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 LIMIT {limit}`,
10741
+ defaultDateRange: "LAST_30_DAYS",
10742
+ defaultLimit: 200,
10743
+ statusFields: ["campaign.status"]
10744
+ },
10745
+ {
10746
+ name: "account-summary",
10747
+ description: "Account-level totals for a date range",
10748
+ gaqlTemplate: `SELECT customer.id, customer.descriptive_name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING {dateRange} LIMIT 1`,
10749
+ defaultDateRange: "LAST_30_DAYS",
10750
+ defaultLimit: 1
10751
+ }
10752
+ ];
10753
+ function getPreset(name) {
10754
+ return PRESETS.find((p) => p.name === name);
10755
+ }
10756
+ function buildStatusScope(statusFields, includePaused) {
10757
+ if (!statusFields || statusFields.length === 0) return "";
10758
+ const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
10759
+ return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
10760
+ }
10761
+ function expandPreset(preset, params) {
10762
+ const dateRange = params.dateRange ?? preset.defaultDateRange;
10763
+ const limit = params.limit ?? preset.defaultLimit;
10764
+ if (!isValidDateRange(dateRange)) {
10765
+ return {
10766
+ query: "",
10767
+ dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
10768
+ };
10769
+ }
10770
+ const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
10771
+ const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
10772
+ return { query };
10773
+ }
10774
+
10628
10775
  // src/commands/ads/google/preflight.ts
10629
10776
  function buildCommand2(query, customerId) {
10630
10777
  return `baker ads google query "${query}" --customer-id ${customerId}`;
10631
10778
  }
10779
+ var RUNG_PIN_FIELDS = {
10780
+ "campaign.status": ["campaign.id", "campaign.resource_name"],
10781
+ "ad_group.status": ["ad_group.id", "ad_group.resource_name"],
10782
+ "ad_group_criterion.status": ["ad_group_criterion.criterion_id", "ad_group_criterion.resource_name"],
10783
+ "ad_group_ad.status": ["ad_group_ad.ad.id", "ad_group_ad.resource_name"],
10784
+ "asset_group.status": ["asset_group.id", "asset_group.resource_name"]
10785
+ };
10786
+ function whereClause(query) {
10787
+ const match = query.match(/\bWHERE\b([\s\S]*)$/i);
10788
+ if (!match?.[1]) return "";
10789
+ let rest = match[1];
10790
+ for (const trailing of [/\bORDER\s+BY\b/i, /\bLIMIT\b/i, /\bPARAMETERS\b/i]) {
10791
+ const idx = rest.search(trailing);
10792
+ if (idx !== -1) rest = rest.slice(0, idx);
10793
+ }
10794
+ return rest;
10795
+ }
10796
+ function selectClause(query) {
10797
+ return query.split(/\bFROM\b/i)[0] ?? "";
10798
+ }
10799
+ function mentions(clause, field) {
10800
+ return new RegExp(`(?<![\\w.])${field.replace(/\./g, "\\.")}\\b`, "i").test(clause);
10801
+ }
10802
+ function analyzeServingChain(query) {
10803
+ const resource = fromResource(query);
10804
+ const chain = resource ? SERVING_CHAINS[resource] : void 0;
10805
+ if (!chain) return null;
10806
+ const where = whereClause(query);
10807
+ if (/ad_group_criterion\.negative\s*=\s*TRUE/i.test(where)) return null;
10808
+ const unscoped = chain.filter((rung) => {
10809
+ if (mentions(where, rung)) return false;
10810
+ return !(RUNG_PIN_FIELDS[rung] ?? []).some((pin) => mentions(where, pin));
10811
+ });
10812
+ const assertsServing = chain.some(
10813
+ (rung) => new RegExp(`${rung.replace(/\./g, "\\.")}\\s*=\\s*'ENABLED'`, "i").test(where)
10814
+ );
10815
+ return { chain, unscoped, assertsServing };
10816
+ }
10817
+ function rejectPartialChain(query, customerId, analysis) {
10818
+ if (!analysis.assertsServing || analysis.unscoped.length === 0) return null;
10819
+ const added = analysis.unscoped.map((rung) => `${rung} = 'ENABLED'`).join(" AND ");
10820
+ const fixed = query.replace(/\bWHERE\b/i, `WHERE ${added} AND`);
10821
+ return {
10822
+ ok: false,
10823
+ error: {
10824
+ code: "INCOMPLETE_STATUS_CHAIN",
10825
+ message: `This query filters on serving status but leaves ${analysis.unscoped.join(", ")} unscoped. Google Ads has no single serving flag \u2014 a row is live only when its whole chain (${analysis.chain.join(" \u2192 ")}) is ENABLED, so rows under a paused parent will come back looking live.`,
10826
+ fix: {
10827
+ action: "retry_with_modified_query",
10828
+ correctedCommand: buildCommand2(fixed, customerId),
10829
+ 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.`
10830
+ },
10831
+ retryable: false
10832
+ }
10833
+ };
10834
+ }
10835
+ function annotateUnscopedChain(query, analysis) {
10836
+ if (analysis.assertsServing || analysis.unscoped.length === 0) return { query, warnings: [] };
10837
+ const select = selectClause(query);
10838
+ const missing = analysis.unscoped.filter((rung) => !mentions(select, rung));
10839
+ if (missing.length === 0) return { query, warnings: [] };
10840
+ return {
10841
+ query: query.replace(/SELECT\s+/i, `SELECT ${missing.join(", ")}, `),
10842
+ warnings: [
10843
+ {
10844
+ code: "SERVING_STATUS_ADDED",
10845
+ message: `Added ${missing.join(", ")} to SELECT \u2014 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.`
10846
+ }
10847
+ ]
10848
+ };
10849
+ }
10632
10850
  var SINGLE_ROW_RESOURCES = ["customer"];
10633
10851
  function fromResource(query) {
10634
10852
  const match = query.match(/\bFROM\s+([A-Za-z_][A-Za-z0-9_]*)/i);
@@ -10641,10 +10859,10 @@ function isSingleRowRead(query) {
10641
10859
  }
10642
10860
  function addRequiredCampaignFields(query) {
10643
10861
  if (!/FROM\s+campaign_budget\b/i.test(query)) return { query, warnings: [] };
10644
- const whereClause2 = query.split(/\bWHERE\b/i)[1] ?? "";
10645
- const selectClause = query.split(/\bFROM\b/i)[0] ?? "";
10646
- const selectFields = new Set(selectClause.match(/campaign\.[\w.]+/g) ?? []);
10647
- const missing = [...new Set(whereClause2.match(/campaign\.[\w.]+/g) ?? [])].filter(
10862
+ const whereClause3 = query.split(/\bWHERE\b/i)[1] ?? "";
10863
+ const selectClause2 = query.split(/\bFROM\b/i)[0] ?? "";
10864
+ const selectFields = new Set(selectClause2.match(/campaign\.[\w.]+/g) ?? []);
10865
+ const missing = [...new Set(whereClause3.match(/campaign\.[\w.]+/g) ?? [])].filter(
10648
10866
  (field) => !selectFields.has(field)
10649
10867
  );
10650
10868
  if (missing.length === 0) return { query, warnings: [] };
@@ -10794,128 +11012,13 @@ function validatePreflight(query, customerId, limit) {
10794
11012
  }
10795
11013
  const { query: autoFixed, warnings: autoFixWarnings } = applyAutoFixes(corrected, limit);
10796
11014
  warnings.push(...autoFixWarnings);
10797
- return { valid: true, correctedQuery: autoFixed, warnings };
10798
- }
10799
-
10800
- // src/commands/ads/google/presets.ts
10801
- var GAQL_DATE_RANGE_RE = /^(?:TODAY|YESTERDAY|LAST_7_DAYS|LAST_14_DAYS|LAST_30_DAYS|LAST_90_DAYS|THIS_MONTH|LAST_MONTH|THIS_QUARTER|LAST_QUARTER|THIS_YEAR|LAST_YEAR|ALL_TIME|BETWEEN\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}'\s+AND\s+'[0-9]{4}-[0-9]{2}-[0-9]{2}')$/i;
10802
- function isValidDateRange(value) {
10803
- return GAQL_DATE_RANGE_RE.test(value.trim());
10804
- }
10805
- var PRESETS = [
10806
- {
10807
- name: "campaign-performance",
10808
- description: "Campaign-level metrics overview",
10809
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.ctr, metrics.average_cpc FROM campaign WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.cost_micros DESC LIMIT {limit}`,
10810
- defaultDateRange: "LAST_30_DAYS",
10811
- defaultLimit: 200,
10812
- statusFields: ["campaign.status"]
10813
- },
10814
- {
10815
- name: "keyword-analysis",
10816
- description: "Keyword performance with match type and quality",
10817
- 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, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10818
- defaultDateRange: "LAST_30_DAYS",
10819
- defaultLimit: 200,
10820
- statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10821
- },
10822
- {
10823
- name: "keyword-serving",
10824
- description: "Why a keyword is limited \u2014 Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score",
10825
- 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 LIMIT {limit}`,
10826
- defaultDateRange: "ALL_TIME",
10827
- defaultLimit: 500,
10828
- statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10829
- },
10830
- {
10831
- name: "positive-keywords",
10832
- description: "Positive (targeting) keywords only \u2014 excludes negatives",
10833
- 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 LIMIT {limit}`,
10834
- defaultDateRange: "ALL_TIME",
10835
- defaultLimit: 500,
10836
- statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
10837
- },
10838
- {
10839
- name: "negative-keywords",
10840
- description: "Negative keywords only \u2014 campaign and ad group level",
10841
- 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 LIMIT {limit}`,
10842
- defaultDateRange: "ALL_TIME",
10843
- defaultLimit: 500
10844
- },
10845
- {
10846
- name: "negative-keyword-lists",
10847
- description: "Shared negative keyword lists and the terms inside them (SharedSet members)",
10848
- gaqlTemplate: `SELECT shared_set.id, shared_set.name, 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 LIMIT {limit}`,
10849
- defaultDateRange: "ALL_TIME",
10850
- defaultLimit: 500
10851
- },
10852
- {
10853
- name: "negative-list-attachments",
10854
- description: "Which campaigns each shared negative keyword list is attached to",
10855
- 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 LIMIT {limit}`,
10856
- defaultDateRange: "ALL_TIME",
10857
- defaultLimit: 500
10858
- },
10859
- {
10860
- name: "search-terms",
10861
- description: "Actual user search queries triggering ads",
10862
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.name, ad_group.status, search_term_view.search_term, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10863
- defaultDateRange: "LAST_7_DAYS",
10864
- defaultLimit: 200,
10865
- statusFields: ["campaign.status", "ad_group.status"]
10866
- },
10867
- {
10868
- name: "ad-copy-performance",
10869
- description: "Ad headline and description effectiveness",
10870
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.status, 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.conversions, metrics.ctr FROM ad_group_ad WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
10871
- defaultDateRange: "LAST_30_DAYS",
10872
- defaultLimit: 200,
10873
- statusFields: ["campaign.status", "ad_group.status", "ad_group_ad.status"]
10874
- },
10875
- {
10876
- name: "asset-performance",
10877
- description: "Performance Max asset performance labels",
10878
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, asset_group.name, asset_group.status, asset_group_asset.field_type, asset_group_asset.performance_label, asset.type, asset.text_asset.text, asset.image_asset.full_size.url FROM asset_group_asset WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING {dateRange}{statusScope} LIMIT {limit}`,
10879
- defaultDateRange: "LAST_30_DAYS",
10880
- defaultLimit: 200,
10881
- statusFields: ["campaign.status", "asset_group.status"]
10882
- },
10883
- {
10884
- name: "shopping-products",
10885
- description: "Product-level shopping performance metrics",
10886
- 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 LIMIT {limit}`,
10887
- defaultDateRange: "LAST_30_DAYS",
10888
- defaultLimit: 200,
10889
- statusFields: ["campaign.status"]
10890
- },
10891
- {
10892
- name: "account-summary",
10893
- description: "Account-level totals for a date range",
10894
- gaqlTemplate: `SELECT customer.id, customer.descriptive_name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.conversions_value FROM customer WHERE segments.date DURING {dateRange} LIMIT 1`,
10895
- defaultDateRange: "LAST_30_DAYS",
10896
- defaultLimit: 1
10897
- }
10898
- ];
10899
- function getPreset(name) {
10900
- return PRESETS.find((p) => p.name === name);
10901
- }
10902
- function buildStatusScope(statusFields, includePaused) {
10903
- if (!statusFields || statusFields.length === 0) return "";
10904
- const predicate = includePaused ? "!= 'REMOVED'" : "= 'ENABLED'";
10905
- return statusFields.map((field) => ` AND ${field} ${predicate}`).join("");
10906
- }
10907
- function expandPreset(preset, params) {
10908
- const dateRange = params.dateRange ?? preset.defaultDateRange;
10909
- const limit = params.limit ?? preset.defaultLimit;
10910
- if (!isValidDateRange(dateRange)) {
10911
- return {
10912
- query: "",
10913
- dateRangeError: `Invalid date range: "${dateRange}". Use a GAQL date literal (e.g. LAST_30_DAYS, TODAY) or BETWEEN 'YYYY-MM-DD' AND 'YYYY-MM-DD'.`
10914
- };
10915
- }
10916
- const statusScope = buildStatusScope(preset.statusFields, params.includePaused ?? false);
10917
- const query = preset.gaqlTemplate.replace(/\{dateRange\}/g, dateRange).replace(/\{limit\}/g, String(limit)).replace(/\{statusScope\}/g, statusScope);
10918
- return { query };
11015
+ const analysis = analyzeServingChain(autoFixed);
11016
+ if (!analysis) return { valid: true, correctedQuery: autoFixed, warnings };
11017
+ const rejection = rejectPartialChain(autoFixed, customerId, analysis);
11018
+ if (rejection) return { valid: false, warnings: [], error: rejection };
11019
+ const annotated = annotateUnscopedChain(autoFixed, analysis);
11020
+ warnings.push(...annotated.warnings);
11021
+ return { valid: true, correctedQuery: annotated.query, warnings };
10919
11022
  }
10920
11023
 
10921
11024
  // src/commands/ads/google/query.ts
@@ -10944,12 +11047,17 @@ registerSchema({
10944
11047
  description: "Include paused entities in preset results. Off by default: presets return only actually-serving entities. Google Ads has no single serving flag \u2014 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.",
10945
11048
  required: false
10946
11049
  },
10947
- limit: { type: "number", description: "Max rows (default 200)", required: false, default: 200 },
11050
+ limit: {
11051
+ type: "number",
11052
+ description: "Max rows in ONE page (default 200), AND the LIMIT added to a query that has none \u2014 so it caps the whole read with --all too. Values up to 10000 are accepted. Any read that fills this cap comes back flagged TRUNCATED_RESULTS \u2014 never count or total from one.",
11053
+ required: false,
11054
+ default: 200
11055
+ },
10948
11056
  "list-presets": { type: "boolean", description: "List all available query presets", required: false },
10949
11057
  cursor: { type: "string", description: "Pagination cursor from previous response", required: false },
10950
11058
  all: {
10951
11059
  type: "boolean",
10952
- description: "Auto-paginate all results (use with --out for large datasets)",
11060
+ description: "Follow every page until the data set is exhausted (use with --out for large datasets). Needed for a read you may count or total, but not sufficient: it exhausts the pages, not the statement's LIMIT, so pair it with a --limit above the row count you expect (or your own LIMIT) and check the read came back without TRUNCATED_RESULTS.",
10953
11061
  required: false
10954
11062
  },
10955
11063
  out: {
@@ -11004,53 +11112,117 @@ function writeRowsToFile(filePath, rows, fields, append) {
11004
11112
  writeFileSync(filePath, JSON.stringify(rows, null, 2), "utf-8");
11005
11113
  }
11006
11114
  }
11115
+ function effectiveRowLimit(finalQuery, limit, all = false) {
11116
+ const match = finalQuery.match(/\bLIMIT\s+(\d+)/i);
11117
+ const statementLimit = match?.[1] ? Number(match[1]) : void 0;
11118
+ if (all) return statementLimit;
11119
+ return statementLimit === void 0 ? limit : Math.min(limit, statementLimit);
11120
+ }
11121
+ function warningLines(warnings, format) {
11122
+ return warnings.map((w) => {
11123
+ const message = w.message.replace(/\s*\n\s*/g, " ");
11124
+ if (format === "jsonl") return JSON.stringify({ _baker_warning: { code: w.code, message } });
11125
+ if (format === "md") return `> **${w.code}** \u2014 ${message}`;
11126
+ return `# baker ${w.code}: ${message}`;
11127
+ });
11128
+ }
11129
+ function completenessWarnings(read) {
11130
+ const cap3 = read.effectiveLimit;
11131
+ const filledTheCap = cap3 !== void 0 && cap3 > 0 && read.rowCount >= cap3;
11132
+ if (!read.hasMore && !filledTheCap) return [];
11133
+ const cause = read.hasMore ? `More rows are waiting: the account returned a next-page cursor this run did not follow.` : `The ${read.rowCount} rows returned exactly fill the row cap in force (${cap3}).`;
11134
+ const fix = read.all ? `Every page was followed, so the cap is the LIMIT in the statement itself \u2014 raise it above ${cap3}, or drop it and pass a larger --limit, then re-run.` : `Re-run with --all --out <file> to page through everything, or raise --limit above ${cap3}.`;
11135
+ return [
11136
+ {
11137
+ code: "TRUNCATED_RESULTS",
11138
+ message: `INCOMPLETE READ \u2014 ${cause} These rows are a page, not the data set. Do NOT count, total or describe them as the whole picture. ${fix}`
11139
+ }
11140
+ ];
11141
+ }
11007
11142
  function buildQueryEnvelope(rows, fieldDescs, warnings, options) {
11008
- const envelope = { ok: true, data: rows };
11009
- if (options.full && Object.keys(fieldDescs).length > 0) {
11010
- envelope.fields = fieldDescs;
11011
- }
11012
- if (options.nextCursor) {
11013
- envelope.pagination = { hasMore: true, cursor: options.nextCursor };
11014
- }
11015
- if (warnings.length > 0) {
11016
- envelope.warnings = warnings;
11017
- }
11018
- if (options.cached) {
11019
- envelope.cached = true;
11020
- }
11021
- return envelope;
11143
+ const notice = summariseWarnings(warnings);
11144
+ return {
11145
+ ok: true,
11146
+ ...options.cached ? { cached: true } : {},
11147
+ ...warnings.length > 0 ? { warnings } : {},
11148
+ ...options.nextCursor ? { pagination: { hasMore: true, cursor: options.nextCursor } } : {},
11149
+ data: rows,
11150
+ ...options.full && Object.keys(fieldDescs).length > 0 ? { fields: fieldDescs } : {},
11151
+ ...notice ? { notice } : {}
11152
+ };
11022
11153
  }
11023
- function outputResults(rows, fieldDescs, warnings, args, cached, nextCursor) {
11024
- const format = args.output || "json";
11025
- const fields = Object.keys(rows[0] ?? {});
11026
- if (format === "csv") {
11027
- process.stdout.write(`${toCsvRow(fields)}
11028
- `);
11029
- for (const row of rows) {
11030
- process.stdout.write(`${toCsvRow(fields.map((f) => String(row[f] ?? "")))}
11154
+ function summariseWarnings(warnings) {
11155
+ if (warnings.length === 0) return void 0;
11156
+ const codes = [...new Set(warnings.map((w) => w.code))].join(", ");
11157
+ return `Read the "warnings" above before using these rows: ${codes}.`;
11158
+ }
11159
+ function buildFileSummary(input) {
11160
+ const notice = summariseWarnings(input.warnings);
11161
+ return {
11162
+ ok: true,
11163
+ fields: {},
11164
+ file: input.file,
11165
+ rows: input.rows,
11166
+ complete: input.complete,
11167
+ ...input.warnings.length > 0 ? { warnings: input.warnings } : {},
11168
+ ...notice ? { notice } : {}
11169
+ };
11170
+ }
11171
+ var REPEAT_NOTICE_ABOVE_ROWS = 20;
11172
+ function emitToStderr(notices) {
11173
+ for (const line of notices) process.stderr.write(`${line}
11031
11174
  `);
11032
- }
11033
- return;
11034
- }
11035
- if (format === "jsonl") {
11036
- for (const row of rows) {
11037
- process.stdout.write(`${JSON.stringify(row)}
11175
+ }
11176
+ function writeLine(line) {
11177
+ process.stdout.write(`${line}
11038
11178
  `);
11039
- }
11040
- return;
11179
+ }
11180
+ var writeCsvRows = (rows, fields, notices) => {
11181
+ writeLine(toCsvRow(fields));
11182
+ for (const line of notices) writeLine(line);
11183
+ for (const row of rows) writeLine(toCsvRow(fields.map((f) => String(row[f] ?? ""))));
11184
+ };
11185
+ var writeJsonlRows = (rows, _fields, notices) => {
11186
+ for (const line of notices) writeLine(line);
11187
+ for (const row of rows) writeLine(JSON.stringify(row));
11188
+ };
11189
+ var writeMdRows = (rows, fields, notices) => {
11190
+ for (const line of notices) writeLine(line);
11191
+ writeLine(`| ${fields.join(" | ")} |`);
11192
+ writeLine(`| ${fields.map(() => "---").join(" | ")} |`);
11193
+ for (const row of rows) writeLine(`| ${fields.map((f) => String(row[f] ?? "")).join(" | ")} |`);
11194
+ };
11195
+ var ROW_WRITERS = { csv: writeCsvRows, jsonl: writeJsonlRows, md: writeMdRows };
11196
+ function writeRows(format, rows, notices) {
11197
+ const write2 = ROW_WRITERS[format] ?? writeMdRows;
11198
+ write2(rows, Object.keys(rows[0] ?? {}), notices);
11199
+ if (rows.length > REPEAT_NOTICE_ABOVE_ROWS) {
11200
+ for (const line of notices) writeLine(line);
11041
11201
  }
11042
- if (format === "md") {
11043
- process.stdout.write(`| ${fields.join(" | ")} |
11044
- `);
11045
- process.stdout.write(`| ${fields.map(() => "---").join(" | ")} |
11046
- `);
11047
- for (const row of rows) {
11048
- process.stdout.write(`| ${fields.map((f) => String(row[f] ?? "")).join(" | ")} |
11049
- `);
11050
- }
11202
+ }
11203
+ function outputResults(rows, fieldDescs, warnings, args, cached, read) {
11204
+ const format = args.output || "json";
11205
+ const allWarnings = [
11206
+ ...warnings,
11207
+ ...completenessWarnings({
11208
+ rowCount: rows.length,
11209
+ effectiveLimit: read.effectiveLimit,
11210
+ all: read.all,
11211
+ hasMore: Boolean(read.nextCursor)
11212
+ })
11213
+ ];
11214
+ emitToStderr(warningLines(allWarnings, "csv"));
11215
+ if (format === "json") {
11216
+ writeAdsJson(
11217
+ buildQueryEnvelope(rows, fieldDescs, allWarnings, {
11218
+ full: Boolean(args.full),
11219
+ cached,
11220
+ nextCursor: read.nextCursor
11221
+ })
11222
+ );
11051
11223
  return;
11052
11224
  }
11053
- writeAdsJson(buildQueryEnvelope(rows, fieldDescs, warnings, { full: Boolean(args.full), cached, nextCursor }));
11225
+ writeRows(format, rows, warningLines(allWarnings, format));
11054
11226
  }
11055
11227
  function resolveGaql(args, limit) {
11056
11228
  if (args.preset) {
@@ -11099,8 +11271,8 @@ var SERVING_HIERARCHY_RESOURCES = [
11099
11271
  "search_term_view",
11100
11272
  "shopping_performance_view",
11101
11273
  "asset_group_asset"
11102
- ];
11103
- function whereClause(upperQuery) {
11274
+ ].filter((resource) => !(resource in SERVING_CHAINS));
11275
+ function whereClause2(upperQuery) {
11104
11276
  const whereIdx = upperQuery.indexOf(" WHERE ");
11105
11277
  if (whereIdx === -1) return "";
11106
11278
  let rest = upperQuery.slice(whereIdx + 7);
@@ -11124,7 +11296,7 @@ function servingScopeWarnings(args, finalQuery) {
11124
11296
  const fromMatch = upper2.match(/\bFROM\s+([A-Z_]+)/);
11125
11297
  const resource = fromMatch?.[1]?.toLowerCase();
11126
11298
  const hierarchical = resource ? SERVING_HIERARCHY_RESOURCES.includes(resource) : false;
11127
- const filtersStatus = whereClause(upper2).includes(".STATUS");
11299
+ const filtersStatus = whereClause2(upper2).includes(".STATUS");
11128
11300
  if (hierarchical && !filtersStatus) {
11129
11301
  return [
11130
11302
  {
@@ -11141,11 +11313,12 @@ function parseSelectedFields(gaql) {
11141
11313
  if (!list) return [];
11142
11314
  return list.split(",").map((field) => field.trim()).filter((field) => /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*$/.test(field));
11143
11315
  }
11144
- function unobservedSelectedFields(gaql, rows) {
11145
- const observed = /* @__PURE__ */ new Set();
11316
+ function foldObservedFields(rows, observed) {
11146
11317
  for (const row of rows) {
11147
11318
  for (const key of Object.keys(row)) observed.add(key);
11148
11319
  }
11320
+ }
11321
+ function unobservedFields(gaql, observed) {
11149
11322
  return parseSelectedFields(gaql).filter((field) => {
11150
11323
  if (field.endsWith(".resource_name")) return false;
11151
11324
  if (observed.has(field)) return false;
@@ -11153,8 +11326,13 @@ function unobservedSelectedFields(gaql, rows) {
11153
11326
  });
11154
11327
  }
11155
11328
  function missingFieldWarnings(gaql, rows) {
11156
- if (rows.length === 0) return [];
11157
- const missing = unobservedSelectedFields(gaql, rows);
11329
+ const observed = /* @__PURE__ */ new Set();
11330
+ foldObservedFields(rows, observed);
11331
+ return missingFieldWarningsFor(gaql, observed, rows.length);
11332
+ }
11333
+ function missingFieldWarningsFor(gaql, observed, rowCount2) {
11334
+ if (rowCount2 === 0) return [];
11335
+ const missing = unobservedFields(gaql, observed);
11158
11336
  if (missing.length === 0) return [];
11159
11337
  return [
11160
11338
  {
@@ -11171,6 +11349,7 @@ async function fetchAllPages(body, args, servingOptions) {
11171
11349
  let pageToken = args.cursor;
11172
11350
  let totalRowsWritten = 0;
11173
11351
  let keywordLimits;
11352
+ const observedFields = /* @__PURE__ */ new Set();
11174
11353
  do {
11175
11354
  const requestBody2 = { ...body };
11176
11355
  if (pageToken) requestBody2.pageToken = pageToken;
@@ -11178,11 +11357,11 @@ async function fetchAllPages(body, args, servingOptions) {
11178
11357
  const rows = response.rows ?? [];
11179
11358
  pageToken = response.pageToken;
11180
11359
  if (args.out) {
11181
- const filePath = resolve(args.out);
11182
11360
  const shouldAppend = args.append || totalRowsWritten > 0;
11183
- writeRowsToFile(filePath, rows, extractFields(rows), shouldAppend);
11361
+ writeRowsToFile(resolve(args.out), rows, extractFields(rows), shouldAppend);
11184
11362
  totalRowsWritten += rows.length;
11185
11363
  keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
11364
+ foldObservedFields(rows, observedFields);
11186
11365
  } else {
11187
11366
  allRows = allRows.concat(rows);
11188
11367
  }
@@ -11192,7 +11371,8 @@ async function fetchAllPages(body, args, servingOptions) {
11192
11371
  allRows,
11193
11372
  lastPageToken: pageToken,
11194
11373
  totalRowsWritten,
11195
- keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions)
11374
+ keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions),
11375
+ observedFields
11196
11376
  };
11197
11377
  }
11198
11378
  async function executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey) {
@@ -11202,12 +11382,33 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
11202
11382
  if (args.cursor) body.pageToken = args.cursor;
11203
11383
  if (!useCache) body.skipCache = true;
11204
11384
  const servingOptions = keywordServingOptions(finalQuery);
11205
- const { allRows, lastPageToken, totalRowsWritten, keywordLimits } = await fetchAllPages(body, args, servingOptions);
11385
+ const { allRows, lastPageToken, totalRowsWritten, keywordLimits, observedFields } = await fetchAllPages(
11386
+ body,
11387
+ args,
11388
+ servingOptions
11389
+ );
11206
11390
  if (args.out) {
11207
- const summary = { ok: true, fields: {}, file: resolve(args.out), rows: totalRowsWritten };
11208
- const limitWarnings = renderKeywordLimits(keywordLimits);
11209
- if (limitWarnings.length > 0) summary.warnings = limitWarnings;
11210
- writeAdsJson(summary);
11391
+ const truncation = completenessWarnings({
11392
+ rowCount: totalRowsWritten,
11393
+ effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
11394
+ all: Boolean(args.all),
11395
+ hasMore: Boolean(lastPageToken)
11396
+ });
11397
+ const fileWarnings = [
11398
+ ...warnings,
11399
+ ...renderKeywordLimits(keywordLimits),
11400
+ ...missingFieldWarningsFor(finalQuery, observedFields, totalRowsWritten),
11401
+ ...truncation
11402
+ ];
11403
+ emitToStderr(warningLines(fileWarnings, "csv"));
11404
+ writeAdsJson(
11405
+ buildFileSummary({
11406
+ file: resolve(args.out),
11407
+ rows: totalRowsWritten,
11408
+ warnings: fileWarnings,
11409
+ complete: truncation.length === 0
11410
+ })
11411
+ );
11211
11412
  return;
11212
11413
  }
11213
11414
  if (useCache && allRows.length > 0) {
@@ -11221,9 +11422,16 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
11221
11422
  [...warnings, ...missingFieldWarnings(finalQuery, allRows), ...keywordServingWarnings(allRows, servingOptions)],
11222
11423
  args,
11223
11424
  false,
11224
- lastPageToken
11425
+ {
11426
+ effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)),
11427
+ all: Boolean(args.all),
11428
+ nextCursor: lastPageToken
11429
+ }
11225
11430
  );
11226
11431
  }
11432
+ function shouldReadCache(args) {
11433
+ return !args["no-cache"] && !args.out;
11434
+ }
11227
11435
  async function validateArgs(args) {
11228
11436
  const customerId = await resolveCustomerId(args);
11229
11437
  const limit = args.limit ? Number(args.limit) : 200;
@@ -11286,10 +11494,14 @@ Examples:
11286
11494
  description: "Include paused entities in preset results (default: serving entities only)",
11287
11495
  required: false
11288
11496
  },
11289
- limit: { type: "string", description: "Max rows (default 200)", required: false },
11497
+ limit: {
11498
+ type: "string",
11499
+ description: "Max rows in one page (default 200); without --all the read stops there",
11500
+ required: false
11501
+ },
11290
11502
  "list-presets": { type: "boolean", description: "List available presets", required: false },
11291
11503
  cursor: { type: "string", description: "Pagination cursor", required: false },
11292
- all: { type: "boolean", description: "Auto-paginate all results", required: false },
11504
+ all: { type: "boolean", description: "Follow every page (use with --out for large datasets)", required: false },
11293
11505
  out: { type: "string", description: "File path for output", required: false },
11294
11506
  append: { type: "boolean", description: "Append to file", required: false },
11295
11507
  output: { type: "string", description: "Format: json|csv|jsonl|md", required: false, default: "json" },
@@ -11313,7 +11525,7 @@ Examples:
11313
11525
  const validated = await validateArgs(args);
11314
11526
  if (!validated) return;
11315
11527
  const { customerId, limit, finalQuery, warnings, useCache, cacheKey } = validated;
11316
- if (useCache) {
11528
+ if (shouldReadCache(args)) {
11317
11529
  const cached = cacheGet("queries", cacheKey);
11318
11530
  if (cached) {
11319
11531
  const fields = extractFields(cached.data);
@@ -11326,7 +11538,8 @@ Examples:
11326
11538
  ...keywordServingWarnings(cached.data, keywordServingOptions(finalQuery))
11327
11539
  ],
11328
11540
  args,
11329
- true
11541
+ true,
11542
+ { effectiveLimit: effectiveRowLimit(finalQuery, limit, Boolean(args.all)), all: Boolean(args.all) }
11330
11543
  );
11331
11544
  return;
11332
11545
  }
@@ -13402,9 +13615,16 @@ var customerIdArg = { "customer-id": { type: "string", description: "10-digit Go
13402
13615
  var fileArg = {
13403
13616
  file: { type: "string", description: "JSON file with the full op payload (flags override)" }
13404
13617
  };
13618
+ var BUDGET_DEMAND_HINTS = [
13619
+ "Check this daily budget against the search volume you measured for these keywords, not against the plan you had before you measured it. A budget many times larger than the traffic the target pool can absorb never spends, and reads to the client as a number nobody costed.",
13620
+ "No volume measured yet? Pull it first \u2014 `baker ads google query --preset search-terms` for what the account already captures, `baker ads google keywords metrics` for the market estimate (those are planner figures, never account performance)."
13621
+ ];
13405
13622
  var budgetsCommand = defineCommand30({
13406
13623
  meta: { name: "budgets", description: "Stage campaign budget create/update" },
13407
13624
  subCommands: {
13625
+ // Fires where the budget is actually set, rather than on whatever the chat happened to stage
13626
+ // first. A budget several times what the target pool can absorb never spends, and reads to the
13627
+ // client as a plan nobody costed.
13408
13628
  create: defineCommand30({
13409
13629
  meta: { name: "create", description: "Stage a campaign budget" },
13410
13630
  args: {
@@ -13423,7 +13643,7 @@ var budgetsCommand = defineCommand30({
13423
13643
  deliveryMethod: args.delivery,
13424
13644
  explicitlyShared: args.shared || void 0
13425
13645
  });
13426
- await stageCreate("google.budget.create", customerId, payload);
13646
+ await stageCreate("google.budget.create", customerId, payload, BUDGET_DEMAND_HINTS);
13427
13647
  }
13428
13648
  }),
13429
13649
  update: defineCommand30({
@@ -13443,7 +13663,7 @@ var budgetsCommand = defineCommand30({
13443
13663
  amountMicros: microsFlag(args.amount, "--amount"),
13444
13664
  deliveryMethod: args.delivery
13445
13665
  });
13446
- await stageUpdate("google.budget.update", customerId, target, payload);
13666
+ await stageUpdate("google.budget.update", customerId, target, payload, BUDGET_DEMAND_HINTS);
13447
13667
  }
13448
13668
  })
13449
13669
  }
@@ -14916,19 +15136,19 @@ function failWriteValidation2(message) {
14916
15136
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
14917
15137
  process.exit(1);
14918
15138
  }
14919
- function loadJsonFileArg2(path40) {
14920
- if (typeof path40 !== "string" || path40.length === 0) {
15139
+ function loadJsonFileArg2(path38) {
15140
+ if (typeof path38 !== "string" || path38.length === 0) {
14921
15141
  return {};
14922
15142
  }
14923
15143
  try {
14924
- const parsed = JSON.parse(readFileSync4(path40, "utf8"));
15144
+ const parsed = JSON.parse(readFileSync4(path38, "utf8"));
14925
15145
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
14926
- failWriteValidation2(`${path40} must contain a JSON object`);
15146
+ failWriteValidation2(`${path38} must contain a JSON object`);
14927
15147
  }
14928
15148
  return parsed;
14929
15149
  } catch (err) {
14930
15150
  if (err instanceof SyntaxError) {
14931
- failWriteValidation2(`${path40} is not valid JSON: ${err.message}`);
15151
+ failWriteValidation2(`${path38} is not valid JSON: ${err.message}`);
14932
15152
  }
14933
15153
  throw err;
14934
15154
  }
@@ -15013,15 +15233,15 @@ function parseLocaleFlag(value) {
15013
15233
  }
15014
15234
  return { language: match[1], country: match[2].toUpperCase() };
15015
15235
  }
15016
- function loadTargetingFileArg(path40) {
15017
- if (typeof path40 !== "string" || path40.length === 0) {
15236
+ function loadTargetingFileArg(path38) {
15237
+ if (typeof path38 !== "string" || path38.length === 0) {
15018
15238
  return void 0;
15019
15239
  }
15020
- const parsed = loadJsonFileArg2(path40);
15240
+ const parsed = loadJsonFileArg2(path38);
15021
15241
  const criteria = parsed.targetingCriteria ?? parsed;
15022
15242
  if (!criteria.include) {
15023
15243
  failWriteValidation2(
15024
- `${path40} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
15244
+ `${path38} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
15025
15245
  );
15026
15246
  }
15027
15247
  return criteria;
@@ -15056,14 +15276,14 @@ function parseCsvLine(line) {
15056
15276
  cells.push(current);
15057
15277
  return cells.map((cell2) => cell2.trim());
15058
15278
  }
15059
- function parseListFileArg(path40, maxRows) {
15060
- if (typeof path40 !== "string" || path40.length === 0) {
15279
+ function parseListFileArg(path38, maxRows) {
15280
+ if (typeof path38 !== "string" || path38.length === 0) {
15061
15281
  return void 0;
15062
15282
  }
15063
- const raw = readFileSync4(path40, "utf8");
15283
+ const raw = readFileSync4(path38, "utf8");
15064
15284
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
15065
15285
  if (lines.length < 2) {
15066
- failWriteValidation2(`${path40} needs a header row and at least one data row`);
15286
+ failWriteValidation2(`${path38} needs a header row and at least one data row`);
15067
15287
  }
15068
15288
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
15069
15289
  const rows = [];
@@ -15082,7 +15302,7 @@ function parseListFileArg(path40, maxRows) {
15082
15302
  }
15083
15303
  }
15084
15304
  if (rows.length > maxRows) {
15085
- failWriteValidation2(`${path40} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
15305
+ failWriteValidation2(`${path38} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
15086
15306
  }
15087
15307
  return { columns, rows };
15088
15308
  }
@@ -15178,11 +15398,11 @@ function readPositionals(args) {
15178
15398
  function splitIdList(raw) {
15179
15399
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
15180
15400
  }
15181
- function idsFileEntries(path40) {
15182
- if (typeof path40 !== "string" || path40.length === 0) {
15401
+ function idsFileEntries(path38) {
15402
+ if (typeof path38 !== "string" || path38.length === 0) {
15183
15403
  return [];
15184
15404
  }
15185
- return readFileSync4(path40, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
15405
+ return readFileSync4(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
15186
15406
  }
15187
15407
  function requireTargets(args, entity) {
15188
15408
  const positionals = readPositionals(args);
@@ -17803,9 +18023,9 @@ function compactRow(row) {
17803
18023
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
17804
18024
  };
17805
18025
  }
17806
- function readPath(row, path40) {
18026
+ function readPath(row, path38) {
17807
18027
  let current = row;
17808
- for (const segment of path40.split(".")) {
18028
+ for (const segment of path38.split(".")) {
17809
18029
  const record = asRecord2(current);
17810
18030
  if (!record) return void 0;
17811
18031
  current = record[segment];
@@ -17815,10 +18035,10 @@ function readPath(row, path40) {
17815
18035
  function projectFields(rows, paths) {
17816
18036
  return rows.map((row) => {
17817
18037
  const projected = {};
17818
- for (const path40 of paths) {
17819
- const value = readPath(row, path40);
18038
+ for (const path38 of paths) {
18039
+ const value = readPath(row, path38);
17820
18040
  if (value !== void 0) {
17821
- projected[path40] = value;
18041
+ projected[path38] = value;
17822
18042
  }
17823
18043
  }
17824
18044
  return projected;
@@ -19118,11 +19338,11 @@ var updateStatusSchema = z25.enum(UPDATE_STATUSES);
19118
19338
  function currencyMinimums2(currencyCode) {
19119
19339
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
19120
19340
  }
19121
- function validateDailyBudgetFloor(money, ctx, path40) {
19341
+ function validateDailyBudgetFloor(money, ctx, path38) {
19122
19342
  if (money?.currencyCode) {
19123
19343
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
19124
19344
  if (Number(money.amount) < min) {
19125
- ctx.addIssue({ code: "custom", path: path40, message: `below the ${min} ${money.currencyCode} daily minimum` });
19345
+ ctx.addIssue({ code: "custom", path: path38, message: `below the ${min} ${money.currencyCode} daily minimum` });
19126
19346
  }
19127
19347
  }
19128
19348
  }
@@ -19791,19 +20011,19 @@ function failWriteValidation3(message) {
19791
20011
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
19792
20012
  process.exit(1);
19793
20013
  }
19794
- function loadJsonFileArg3(path40) {
19795
- if (typeof path40 !== "string" || path40.length === 0) {
20014
+ function loadJsonFileArg3(path38) {
20015
+ if (typeof path38 !== "string" || path38.length === 0) {
19796
20016
  return {};
19797
20017
  }
19798
20018
  try {
19799
- const parsed = JSON.parse(readFileSync8(path40, "utf8"));
20019
+ const parsed = JSON.parse(readFileSync8(path38, "utf8"));
19800
20020
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
19801
- failWriteValidation3(`${path40} must contain a JSON object`);
20021
+ failWriteValidation3(`${path38} must contain a JSON object`);
19802
20022
  }
19803
20023
  return parsed;
19804
20024
  } catch (err) {
19805
20025
  if (err instanceof SyntaxError) {
19806
- failWriteValidation3(`${path40} is not valid JSON: ${err.message}`);
20026
+ failWriteValidation3(`${path38} is not valid JSON: ${err.message}`);
19807
20027
  }
19808
20028
  throw err;
19809
20029
  }
@@ -24401,11 +24621,11 @@ function unwrap(response) {
24401
24621
  }
24402
24622
  return response.data;
24403
24623
  }
24404
- async function readAvatars(path40, params) {
24405
- return unwrap(await apiGet(path40, params));
24624
+ async function readAvatars(path38, params) {
24625
+ return unwrap(await apiGet(path38, params));
24406
24626
  }
24407
- async function writeAvatars(path40, body) {
24408
- return unwrap(await apiPost(path40, body));
24627
+ async function writeAvatars(path38, body) {
24628
+ return unwrap(await apiPost(path38, body));
24409
24629
  }
24410
24630
 
24411
24631
  // src/commands/avatars/create.ts
@@ -25151,14 +25371,14 @@ function suggestFamilies(wanted, catalogue, limit = 5) {
25151
25371
  const key = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
25152
25372
  const target = key(wanted);
25153
25373
  if (!target) return [];
25154
- const words3 = target.split(" ");
25374
+ const words2 = target.split(" ");
25155
25375
  const scored = catalogue.map((family) => {
25156
25376
  const candidate = key(family);
25157
25377
  if (candidate === target) return { family, score: 0 };
25158
25378
  if (candidate.startsWith(target)) return { family, score: 1 };
25159
25379
  if (candidate.includes(target)) return { family, score: 2 };
25160
- const shared = words3.filter((w) => w.length > 2 && candidate.includes(w)).length;
25161
- return { family, score: shared > 0 ? 3 + (words3.length - shared) : Number.POSITIVE_INFINITY };
25380
+ const shared = words2.filter((w) => w.length > 2 && candidate.includes(w)).length;
25381
+ return { family, score: shared > 0 ? 3 + (words2.length - shared) : Number.POSITIVE_INFINITY };
25162
25382
  }).filter((c) => Number.isFinite(c.score)).sort((a, b) => a.score - b.score || a.family.localeCompare(b.family));
25163
25383
  return scored.slice(0, limit).map((c) => c.family);
25164
25384
  }
@@ -25239,12 +25459,12 @@ function missingFontFiles(urls, available) {
25239
25459
  function planFontAdoption(sources, families) {
25240
25460
  const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
25241
25461
  const byFamily = /* @__PURE__ */ new Map();
25242
- for (const { path: path40, source } of sources) {
25243
- const dir = posix.dirname(path40);
25462
+ for (const { path: path38, source } of sources) {
25463
+ const dir = posix.dirname(path38);
25244
25464
  for (const face of declaredFontFaces(source)) {
25245
25465
  if (!wanted.has(face.family)) continue;
25246
25466
  const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
25247
- perFile.set(path40, [...perFile.get(path40) ?? [], rebaseFontFaceSrc(face.block, dir)]);
25467
+ perFile.set(path38, [...perFile.get(path38) ?? [], rebaseFontFaceSrc(face.block, dir)]);
25248
25468
  byFamily.set(face.family, perFile);
25249
25469
  }
25250
25470
  }
@@ -26652,10 +26872,10 @@ function estSpeechS(text2) {
26652
26872
  var OBSERVED_WPS_MIN = 1;
26653
26873
  var OBSERVED_WPS_MAX = 6;
26654
26874
  function estSpeechWindowS(text2, startS, endS) {
26655
- const words3 = wordCount(text2);
26875
+ const words2 = wordCount(text2);
26656
26876
  const window2 = (endS ?? 0) - (startS ?? 0);
26657
- if (words3 > 0 && window2 > 0.3) {
26658
- const wps = words3 / window2;
26877
+ if (words2 > 0 && window2 > 0.3) {
26878
+ const wps = words2 / window2;
26659
26879
  if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
26660
26880
  }
26661
26881
  return estSpeechS(text2);
@@ -27511,10 +27731,10 @@ function scrubFloatSentences(text2, floatDescs) {
27511
27731
  if (floatDescs.length === 0 || !text2) return text2;
27512
27732
  const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
27513
27733
  const kept = text2.split(/(?<=[.!?])\s+/).filter((sentence) => {
27514
- const words3 = new Set(floatTokens(sentence));
27734
+ const words2 = new Set(floatTokens(sentence));
27515
27735
  return !tokenSets.some((ts) => {
27516
27736
  let hits = 0;
27517
- for (const w of words3) if (ts.has(w)) hits++;
27737
+ for (const w of words2) if (ts.has(w)) hits++;
27518
27738
  return hits >= 2;
27519
27739
  });
27520
27740
  }).join(" ").trim();
@@ -33439,12 +33659,12 @@ function collectSideEffects(tree) {
33439
33659
  );
33440
33660
  }
33441
33661
  function readFlowTree(slug) {
33442
- const path40 = join3(flowsDir(), slug, "_data.json");
33443
- if (!existsSync5(path40)) {
33662
+ const path38 = join3(flowsDir(), slug, "_data.json");
33663
+ if (!existsSync5(path38)) {
33444
33664
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
33445
33665
  }
33446
33666
  try {
33447
- return JSON.parse(readFileSync9(path40, "utf-8"));
33667
+ return JSON.parse(readFileSync9(path38, "utf-8"));
33448
33668
  } catch (error) {
33449
33669
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
33450
33670
  }
@@ -33836,10 +34056,10 @@ function parseValueExpression(raw) {
33836
34056
  return parts.map(parsePart);
33837
34057
  }
33838
34058
  function trackingFieldIds() {
33839
- const path40 = join4(flowsDir(), "..", "tracking.ts");
33840
- if (!existsSync6(path40)) return null;
34059
+ const path38 = join4(flowsDir(), "..", "tracking.ts");
34060
+ if (!existsSync6(path38)) return null;
33841
34061
  try {
33842
- const source = readFileSync10(path40, "utf-8");
34062
+ const source = readFileSync10(path38, "utf-8");
33843
34063
  const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
33844
34064
  if (!block2) return null;
33845
34065
  const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
@@ -34391,13 +34611,13 @@ function specsFromFile(parsed) {
34391
34611
  return `${destField}${type}=${entry?.value ?? ""}`;
34392
34612
  });
34393
34613
  }
34394
- function readSpecFile(path40) {
34614
+ function readSpecFile(path38) {
34395
34615
  let raw;
34396
34616
  try {
34397
- raw = path40 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path40, "utf-8");
34617
+ raw = path38 === "-" ? readFileSync11(0, "utf-8") : readFileSync11(path38, "utf-8");
34398
34618
  } catch (error) {
34399
34619
  refuse(
34400
- `Could not read ${path40 === "-" ? "the mapping from stdin" : `"${path40}"`}: ${error instanceof Error ? error.message : String(error)}`
34620
+ `Could not read ${path38 === "-" ? "the mapping from stdin" : `"${path38}"`}: ${error instanceof Error ? error.message : String(error)}`
34401
34621
  );
34402
34622
  }
34403
34623
  let parsed;
@@ -34405,7 +34625,7 @@ function readSpecFile(path40) {
34405
34625
  parsed = JSON.parse(raw);
34406
34626
  } catch (error) {
34407
34627
  refuse(
34408
- `${path40 === "-" ? "stdin" : `"${path40}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
34628
+ `${path38 === "-" ? "stdin" : `"${path38}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
34409
34629
  'Expected { "map": { "<destField>": "<value>", \u2026 } }'
34410
34630
  );
34411
34631
  }
@@ -34659,18 +34879,18 @@ var ARRAY_FIELDS = [
34659
34879
  "tagIds"
34660
34880
  ];
34661
34881
  var ARRAY_OWNERS = ["", "body"];
34662
- function dropUnsetOptionals(sideEffect, path40) {
34882
+ function dropUnsetOptionals(sideEffect, path38) {
34663
34883
  return OPTIONAL_STRINGS.flatMap((key) => {
34664
34884
  if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
34665
34885
  delete sideEffect[key];
34666
- return [{ path: path40, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
34886
+ return [{ path: path38, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
34667
34887
  });
34668
34888
  }
34669
- function fillNulledArrays(target, prefix, path40) {
34889
+ function fillNulledArrays(target, prefix, path38) {
34670
34890
  return ARRAY_FIELDS.flatMap((key) => {
34671
34891
  if (!(key in target) || target[key] !== null) return [];
34672
34892
  target[key] = [];
34673
- return [{ path: path40, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
34893
+ return [{ path: path38, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
34674
34894
  });
34675
34895
  }
34676
34896
  function sideEffectsOf(node) {
@@ -34680,13 +34900,13 @@ function sideEffectsOf(node) {
34680
34900
  );
34681
34901
  }
34682
34902
  function normalizeSideEffect(sideEffect, where) {
34683
- const path40 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
34903
+ const path38 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
34684
34904
  const arrays = ARRAY_OWNERS.flatMap((owner) => {
34685
34905
  const target = owner ? sideEffect[owner] : sideEffect;
34686
34906
  if (!target || typeof target !== "object") return [];
34687
- return fillNulledArrays(target, owner ? `${owner}.` : "", path40);
34907
+ return fillNulledArrays(target, owner ? `${owner}.` : "", path38);
34688
34908
  });
34689
- return [...dropUnsetOptionals(sideEffect, path40), ...arrays];
34909
+ return [...dropUnsetOptionals(sideEffect, path38), ...arrays];
34690
34910
  }
34691
34911
  function normalizeFlowTree(tree) {
34692
34912
  const changes = [];
@@ -35224,10 +35444,10 @@ async function stageOps(ops) {
35224
35444
  handleError2(err);
35225
35445
  }
35226
35446
  }
35227
- async function draftAction2(path40, body, chat) {
35447
+ async function draftAction2(path38, body, chat) {
35228
35448
  const chatId = resolveChatId(chat);
35229
35449
  try {
35230
- const data = await apiPost(path40, { chatId, ...body });
35450
+ const data = await apiPost(path38, { chatId, ...body });
35231
35451
  writeJsonEnvelope({ ok: true, data });
35232
35452
  return data;
35233
35453
  } catch (err) {
@@ -37659,9 +37879,9 @@ async function readImageBuffer(pathOrUrl) {
37659
37879
  }
37660
37880
  return readFile20(pathOrUrl);
37661
37881
  }
37662
- async function isDirectory(path40) {
37882
+ async function isDirectory(path38) {
37663
37883
  try {
37664
- const s = await stat4(path40);
37884
+ const s = await stat4(path38);
37665
37885
  return s.isDirectory();
37666
37886
  } catch {
37667
37887
  return false;
@@ -37963,13 +38183,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
37963
38183
  }
37964
38184
  function disambiguate(paths) {
37965
38185
  const taken = /* @__PURE__ */ new Set();
37966
- return paths.map((path40) => {
37967
- if (!taken.has(path40)) {
37968
- taken.add(path40);
37969
- return path40;
38186
+ return paths.map((path38) => {
38187
+ if (!taken.has(path38)) {
38188
+ taken.add(path38);
38189
+ return path38;
37970
38190
  }
37971
- const ext = extname3(path40);
37972
- const stem = path40.slice(0, path40.length - ext.length);
38191
+ const ext = extname3(path38);
38192
+ const stem = path38.slice(0, path38.length - ext.length);
37973
38193
  let n = 2;
37974
38194
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
37975
38195
  const unique = `${stem}-${n}${ext}`;
@@ -38096,10 +38316,10 @@ async function runDownloads(plan) {
38096
38316
  const paths = disambiguate(fetched.map((item) => item.path));
38097
38317
  const downloaded = [];
38098
38318
  for (const [index, item] of fetched.entries()) {
38099
- const path40 = paths[index] ?? item.path;
38319
+ const path38 = paths[index] ?? item.path;
38100
38320
  try {
38101
- await atomicWrite(path40, item.buffer);
38102
- downloaded.push({ input: item.input, output: path40, bytes: item.buffer.length, contentType: item.contentType });
38321
+ await atomicWrite(path38, item.buffer);
38322
+ downloaded.push({ input: item.input, output: path38, bytes: item.buffer.length, contentType: item.contentType });
38103
38323
  } catch (err) {
38104
38324
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
38105
38325
  }
@@ -41059,7 +41279,7 @@ import { defineCommand as defineCommand166 } from "citty";
41059
41279
 
41060
41280
  // src/commands/landing/critique.ts
41061
41281
  import { readdir as readdir9, stat as stat6 } from "fs/promises";
41062
- import path29 from "path";
41282
+ import path28 from "path";
41063
41283
  import { defineCommand as defineCommand156 } from "citty";
41064
41284
 
41065
41285
  // src/engine/landing/lib/constants.ts
@@ -41262,11 +41482,6 @@ var RULE_META = {
41262
41482
  severity: "block",
41263
41483
  note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
41264
41484
  },
41265
- "copied-reference-copy": {
41266
- family: "originality",
41267
- severity: "block",
41268
- note: "This line is lifted from a section you consulted in the inspiration library. Reference sections are for structure and mechanism, never words \u2014 a visitor who has seen the original reads this as a clone, and the claim is not yours to make. Rewrite it from the client's own offer."
41269
- },
41270
41485
  "broken-image": {
41271
41486
  family: "integrity",
41272
41487
  severity: "block",
@@ -41447,64 +41662,6 @@ var SEVERITY_WEIGHT = {
41447
41662
  advisory: 0.05
41448
41663
  };
41449
41664
 
41450
- // src/engine/landing/lib/originality.ts
41451
- var MIN_COMPARABLE_LENGTH = 12;
41452
- var NEAR_MATCH_RATIO = 0.8;
41453
- function normalize(value) {
41454
- return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
41455
- }
41456
- function words2(value) {
41457
- return normalize(value).split(" ").filter(Boolean);
41458
- }
41459
- function copyOverlapRatio(candidate, reference) {
41460
- const referenceWords = words2(reference);
41461
- if (referenceWords.length === 0) return 0;
41462
- const candidateWords = new Set(words2(candidate));
41463
- const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
41464
- return shared / referenceWords.length;
41465
- }
41466
- function isVerbatimReuse(candidate, reference) {
41467
- const normalizedCandidate = normalize(candidate);
41468
- const normalizedReference = normalize(reference);
41469
- if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
41470
- if (normalizedCandidate.includes(normalizedReference)) return true;
41471
- return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
41472
- }
41473
- function extractVisibleStrings(text2) {
41474
- const found = [];
41475
- const lines = text2.split("\n");
41476
- for (const [index, line] of lines.entries()) {
41477
- for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
41478
- const value = match[1]?.trim();
41479
- if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
41480
- }
41481
- }
41482
- return found;
41483
- }
41484
- function detectOriginality(sources, references) {
41485
- if (references.length === 0) return [];
41486
- const findings = [];
41487
- const seen = /* @__PURE__ */ new Set();
41488
- for (const source of sources) {
41489
- for (const { value, line } of extractVisibleStrings(source.text)) {
41490
- for (const reference of references) {
41491
- const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
41492
- if (!hit) continue;
41493
- const key = `${source.path}:${line}:${normalize(hit)}`;
41494
- if (seen.has(key)) continue;
41495
- seen.add(key);
41496
- findings.push({
41497
- id: "copied-reference-copy",
41498
- snippet: value.slice(0, 120),
41499
- file: source.path,
41500
- line
41501
- });
41502
- }
41503
- }
41504
- }
41505
- return findings;
41506
- }
41507
-
41508
41665
  // src/engine/landing/lib/rules.ts
41509
41666
  var cap2 = (m, i) => m[i] ?? "";
41510
41667
  var num2 = (m, i) => Number(m[i] ?? 0);
@@ -42032,16 +42189,7 @@ function dedupe(findings) {
42032
42189
  }
42033
42190
 
42034
42191
  // src/engine/landing/lib/critique.ts
42035
- var FAMILIES = [
42036
- "typography",
42037
- "color",
42038
- "borders_depth",
42039
- "motion",
42040
- "spacing",
42041
- "copy",
42042
- "integrity",
42043
- "originality"
42044
- ];
42192
+ var FAMILIES = ["typography", "color", "borders_depth", "motion", "spacing", "copy", "integrity"];
42045
42193
  function round4(n) {
42046
42194
  return Math.round(n * 100) / 100;
42047
42195
  }
@@ -42060,7 +42208,6 @@ function critiqueLanding(input) {
42060
42208
  const raws = [];
42061
42209
  for (const source of sources) raws.push(...detectSource(source));
42062
42210
  raws.push(...detectPage(sources));
42063
- raws.push(...detectOriginality(sources, input.references ?? []));
42064
42211
  for (const raw of raws) {
42065
42212
  const meta = RULE_META[raw.id];
42066
42213
  if (!meta) continue;
@@ -42098,82 +42245,41 @@ function describeCounts(findings) {
42098
42245
  return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
42099
42246
  }
42100
42247
 
42101
- // src/engine/landing/lib/referenceStore.ts
42102
- import { mkdir as mkdir8, readFile as readFile21, writeFile as writeFile12 } from "fs/promises";
42103
- import path26 from "path";
42104
- var REFERENCES_FILE = ".cache/inspiration-refs.json";
42105
- var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
42106
- async function readReferences(projectRoot) {
42107
- try {
42108
- const raw = await readFile21(path26.join(projectRoot, REFERENCES_FILE), "utf8");
42109
- const parsed = JSON.parse(raw);
42110
- if (!Array.isArray(parsed)) return [];
42111
- const cutoff = Date.now() - REFERENCE_TTL_MS;
42112
- return parsed.filter((entry) => {
42113
- if (typeof entry !== "object" || entry === null) return false;
42114
- const candidate = entry;
42115
- if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
42116
- const at = Date.parse(candidate.consultedAt ?? "");
42117
- return Number.isNaN(at) ? true : at >= cutoff;
42118
- });
42119
- } catch {
42120
- return [];
42121
- }
42122
- }
42123
- async function recordReference(projectRoot, reference) {
42124
- return await recordReferences(projectRoot, [reference]);
42125
- }
42126
- async function recordReferences(projectRoot, references) {
42127
- if (references.length === 0) return true;
42128
- try {
42129
- const existing = await readReferences(projectRoot);
42130
- const replaced = new Set(references.map((reference) => reference.sectionId));
42131
- const merged = [...existing.filter((entry) => !replaced.has(entry.sectionId)), ...references];
42132
- const file = path26.join(projectRoot, REFERENCES_FILE);
42133
- await mkdir8(path26.dirname(file), { recursive: true });
42134
- await writeFile12(file, `${JSON.stringify(merged, null, 2)}
42135
- `);
42136
- return true;
42137
- } catch {
42138
- return false;
42139
- }
42140
- }
42141
-
42142
42248
  // src/commands/landing/snapshot.ts
42143
- import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
42144
- import path27 from "path";
42249
+ import { mkdir as mkdir8, rename as rename2, writeFile as writeFile12 } from "fs/promises";
42250
+ import path26 from "path";
42145
42251
  var CRITIC_VERSION = "2";
42146
42252
  function critiqueCacheDir(projectRoot) {
42147
- return path27.join(projectRoot, ".cache", "landing-critique");
42253
+ return path26.join(projectRoot, ".cache", "landing-critique");
42148
42254
  }
42149
42255
  function snapshotPath(projectRoot, slug) {
42150
- return path27.join(critiqueCacheDir(projectRoot), `${slug}.json`);
42256
+ return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
42151
42257
  }
42152
42258
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
42153
- await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
42259
+ await mkdir8(critiqueCacheDir(projectRoot), { recursive: true });
42154
42260
  const dest = snapshotPath(projectRoot, snapshot.slug);
42155
42261
  const tmp = `${dest}.tmp`;
42156
- await writeFile13(tmp, `${JSON.stringify(snapshot, null, 2)}
42262
+ await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
42157
42263
  `, "utf8");
42158
42264
  await rename2(tmp, dest);
42159
42265
  }
42160
42266
 
42161
42267
  // src/commands/landing/source-version.ts
42162
- import { readdir as readdir8, readFile as readFile22, stat as stat5 } from "fs/promises";
42163
- import path28 from "path";
42268
+ import { readdir as readdir8, readFile as readFile21, stat as stat5 } from "fs/promises";
42269
+ import path27 from "path";
42164
42270
  async function landingSourceRelPaths(landingDir) {
42165
42271
  const rel = [];
42166
- if (await isFile(path28.join(landingDir, "index.astro"))) rel.push("index.astro");
42167
- const componentsDir = path28.join(landingDir, "_components");
42272
+ if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
42273
+ const componentsDir = path27.join(landingDir, "_components");
42168
42274
  for (const abs of await walkAstro(componentsDir)) {
42169
- rel.push(path28.relative(landingDir, abs).split(path28.sep).join("/"));
42275
+ rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
42170
42276
  }
42171
42277
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
42172
42278
  }
42173
42279
  async function readLandingSources(landingDir) {
42174
42280
  const rel = await landingSourceRelPaths(landingDir);
42175
42281
  const out = [];
42176
- for (const r of rel) out.push({ path: r, text: await readFile22(path28.join(landingDir, r), "utf8") });
42282
+ for (const r of rel) out.push({ path: r, text: await readFile21(path27.join(landingDir, r), "utf8") });
42177
42283
  return out;
42178
42284
  }
42179
42285
  async function computeLandingSourceSha(landingDir) {
@@ -42182,7 +42288,7 @@ async function computeLandingSourceSha(landingDir) {
42182
42288
  for (const r of rel) {
42183
42289
  let bytes;
42184
42290
  try {
42185
- bytes = await readFile22(path28.join(landingDir, r));
42291
+ bytes = await readFile21(path27.join(landingDir, r));
42186
42292
  } catch {
42187
42293
  bytes = Buffer.alloc(0);
42188
42294
  }
@@ -42206,7 +42312,7 @@ async function walkAstro(dir) {
42206
42312
  }
42207
42313
  const out = [];
42208
42314
  for (const entry of entries) {
42209
- const abs = path28.join(dir, entry.name);
42315
+ const abs = path27.join(dir, entry.name);
42210
42316
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
42211
42317
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
42212
42318
  }
@@ -42267,14 +42373,14 @@ var critiqueCommand2 = defineCommand156({
42267
42373
  { availableSlugs: await listLandingSlugs(projectRoot) }
42268
42374
  );
42269
42375
  }
42270
- if (!await isDir(path29.resolve(projectRoot, "src", "pages", slug))) {
42376
+ if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
42271
42377
  fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
42272
42378
  availableSlugs: await listLandingSlugs(projectRoot)
42273
42379
  });
42274
42380
  }
42275
42381
  }
42276
- const [brand, references] = await Promise.all([loadBrandTokens(projectRoot), readReferences(projectRoot)]);
42277
- const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, references)));
42382
+ const brand = await loadBrandTokens(projectRoot);
42383
+ const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
42278
42384
  const landings = results.map(({ slug, report }) => ({
42279
42385
  slug,
42280
42386
  overall: report.overall,
@@ -42305,10 +42411,10 @@ var critiqueCommand2 = defineCommand156({
42305
42411
  );
42306
42412
  }
42307
42413
  });
42308
- async function critiqueOne(projectRoot, slug, brand, references) {
42309
- const landingDir = path29.resolve(projectRoot, "src", "pages", slug);
42414
+ async function critiqueOne(projectRoot, slug, brand) {
42415
+ const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
42310
42416
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
42311
- const report = critiqueLanding({ slug, sources, brand, references });
42417
+ const report = critiqueLanding({ slug, sources, brand });
42312
42418
  let snapshotFailed = false;
42313
42419
  try {
42314
42420
  await writeCritiqueSnapshot(projectRoot, {
@@ -42326,7 +42432,7 @@ async function critiqueOne(projectRoot, slug, brand, references) {
42326
42432
  }
42327
42433
  async function listLandingSlugs(projectRoot) {
42328
42434
  try {
42329
- const entries = await readdir9(path29.join(projectRoot, "src", "pages"), { withFileTypes: true });
42435
+ const entries = await readdir9(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
42330
42436
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
42331
42437
  } catch {
42332
42438
  return [];
@@ -42359,7 +42465,7 @@ import { defineCommand as defineCommand157 } from "citty";
42359
42465
 
42360
42466
  // src/commands/landing/inspiration/shared.ts
42361
42467
  var INSPIRATION_HINTS = {
42362
- adapt: "Reference only. Re-express this in the client's BRAND.md palette, type and imagery register. Reusing a headline, subhead or CTA verbatim is a Tier 0 message-match failure (references/gotchas.md) and `baker landing critique` will block the publish.",
42468
+ adapt: "Reference only. Re-express this in the client's BRAND.md palette, type and imagery register. Reusing a headline, subhead or CTA verbatim is a Tier 0 message-match failure (references/gotchas.md).",
42363
42469
  structureNotCopy: "Take the structural decision, not the furniture: what the eye hits first, the grid ratio, what was deliberately left out. Your copy must come from the client's own offer."
42364
42470
  };
42365
42471
  function favoritesScopeHints(health, resultCount) {
@@ -42477,12 +42583,12 @@ var addCommand = defineCommand157({
42477
42583
  });
42478
42584
 
42479
42585
  // src/commands/landing/inspiration/code.ts
42480
- import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
42481
- import path30 from "path";
42586
+ import { mkdir as mkdir9, writeFile as writeFile13 } from "fs/promises";
42587
+ import path29 from "path";
42482
42588
  import { defineCommand as defineCommand158 } from "citty";
42483
42589
  registerSchema({
42484
42590
  command: "landing.inspiration.code",
42485
- description: "Write a reference section's standalone HTML+CSS to .baker/inspiration/<id>/ so you can read how it is built. Reference only \u2014 the structure is the lesson, the words are not yours to reuse. Consulting a section records it, and `baker landing critique` blocks a publish that ships its copy verbatim.",
42591
+ description: "Write a reference section's standalone HTML+CSS to .baker/inspiration/<id>/ so you can read how it is built. Reference only \u2014 the structure is the lesson, the words are not yours to reuse.",
42486
42592
  args: {
42487
42593
  id: { type: "string", description: "Section id from search", required: true },
42488
42594
  full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
@@ -42501,30 +42607,19 @@ var codeCommand = defineCommand158({
42501
42607
  try {
42502
42608
  const id = args.id;
42503
42609
  const data = await apiGet("/api/landing-inspiration/section-code", { id });
42504
- const dir = path30.join(process.cwd(), ".baker", "inspiration", id);
42505
- await mkdir10(dir, { recursive: true });
42506
- const file = path30.join(dir, "section.html");
42507
- await writeFile14(file, data.html);
42508
- const recorded = await recordReference(process.cwd(), {
42509
- sectionId: id,
42510
- sourceUrl: data.sourceUrl,
42511
- copyStrings: data.copyStrings,
42512
- consultedAt: (/* @__PURE__ */ new Date()).toISOString()
42513
- });
42610
+ const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
42611
+ await mkdir9(dir, { recursive: true });
42612
+ const file = path29.join(dir, "section.html");
42613
+ await writeFile13(file, data.html);
42514
42614
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
42515
42615
  const fidelity = fidelityHint(data.fidelity);
42516
42616
  if (fidelity) hints.push(fidelity);
42517
42617
  if (data.fidelityNote) hints.push(data.fidelityNote);
42518
- if (!recorded) {
42519
- hints.push(
42520
- "Could not record this reference (.cache/ not writable?) \u2014 the originality check at publish will not be able to see it, so be especially careful not to reuse its copy."
42521
- );
42522
- }
42523
42618
  writeJson({
42524
42619
  ok: true,
42525
42620
  data: {
42526
42621
  id,
42527
- file: path30.relative(process.cwd(), file),
42622
+ file: path29.relative(process.cwd(), file),
42528
42623
  bytes: data.html.length,
42529
42624
  fidelity: data.fidelity,
42530
42625
  reproduction_notes: data.reproductionNotes,
@@ -42779,25 +42874,8 @@ var pageCommand2 = defineCommand160({
42779
42874
  });
42780
42875
 
42781
42876
  // src/commands/landing/inspiration/scrape.ts
42782
- import { readFile as readFile23 } from "fs/promises";
42783
- import path34 from "path";
42784
42877
  import { defineCommand as defineCommand161 } from "citty";
42785
42878
 
42786
- // src/engine/landing/lib/capturedReferences.ts
42787
- var MAX_STRINGS_PER_SECTION = 40;
42788
- function bodyOf(markup) {
42789
- const body = markup.indexOf("<body");
42790
- return body === -1 ? markup : markup.slice(body);
42791
- }
42792
- function capturedCopyStrings(markup) {
42793
- const seen = /* @__PURE__ */ new Set();
42794
- for (const { value } of extractVisibleStrings(bodyOf(markup))) {
42795
- seen.add(value);
42796
- if (seen.size >= MAX_STRINGS_PER_SECTION) break;
42797
- }
42798
- return [...seen];
42799
- }
42800
-
42801
42879
  // src/engine/landing-library/proxyFailure.ts
42802
42880
  var PROXY_STATUS = 407;
42803
42881
  var PROXY_NET_ERRORS = [
@@ -42972,8 +43050,8 @@ function classifyCaptureFailure(error) {
42972
43050
  }
42973
43051
 
42974
43052
  // src/engine/landing-library/run.ts
42975
- import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
42976
- import path32 from "path";
43053
+ import { mkdir as mkdir10, writeFile as writeFile15 } from "fs/promises";
43054
+ import path31 from "path";
42977
43055
 
42978
43056
  // ../proxy/src/preflight.ts
42979
43057
  import http from "http";
@@ -44388,11 +44466,11 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
44388
44466
  }
44389
44467
 
44390
44468
  // src/engine/landing-library/report.ts
44391
- import { writeFile as writeFile15 } from "fs/promises";
44392
- import path31 from "path";
44469
+ import { writeFile as writeFile14 } from "fs/promises";
44470
+ import path30 from "path";
44393
44471
  async function writeCaptureReport(manifest, outDir) {
44394
- const file = path31.join(outDir, "report.html");
44395
- await writeFile15(file, renderReport(manifest));
44472
+ const file = path30.join(outDir, "report.html");
44473
+ await writeFile14(file, renderReport(manifest));
44396
44474
  return file;
44397
44475
  }
44398
44476
  function escapeHtml3(value) {
@@ -44570,32 +44648,32 @@ async function reproducePage(args) {
44570
44648
  const { browser, page, outDir, pageUrl, livePageShot } = args;
44571
44649
  const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
44572
44650
  if (!built) return { bundle: null, fidelity: null };
44573
- await writeFile16(path32.join(outDir, "page.html"), built.html);
44651
+ await writeFile15(path31.join(outDir, "page.html"), built.html);
44574
44652
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
44575
44653
  wholePage: true,
44576
44654
  timeoutMs: 6e4
44577
44655
  });
44578
44656
  if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
44579
- await writeFile16(path32.join(outDir, "page-rendered.png"), rendered);
44657
+ await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
44580
44658
  const { score, note } = await scoreFidelity(livePageShot, rendered);
44581
44659
  return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
44582
44660
  }
44583
44661
  async function captureOneSection(args) {
44584
44662
  const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
44585
- const dir = path32.join(sectionsDir, String(candidate.index).padStart(2, "0"));
44586
- await mkdir11(dir, { recursive: true });
44663
+ const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
44664
+ await mkdir10(dir, { recursive: true });
44587
44665
  const desktop = await captureSection(page, candidate);
44588
- if (desktop) await writeFile16(path32.join(dir, "desktop.png"), desktop);
44666
+ if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
44589
44667
  const visualHash = desktop ? await perceptualHash(desktop) : null;
44590
44668
  const motion = await collectMotion(page, candidate.selector);
44591
44669
  const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
44592
44670
  let fidelity = null;
44593
44671
  let fidelityNote;
44594
44672
  if (built) {
44595
- await writeFile16(path32.join(dir, "section.html"), built.html);
44673
+ await writeFile15(path31.join(dir, "section.html"), built.html);
44596
44674
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
44597
44675
  if (rendered && desktop) {
44598
- await writeFile16(path32.join(dir, "section-rendered.png"), rendered);
44676
+ await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
44599
44677
  const result = await scoreFidelity(desktop, rendered);
44600
44678
  fidelity = result.score;
44601
44679
  fidelityNote = result.note;
@@ -44603,9 +44681,9 @@ async function captureOneSection(args) {
44603
44681
  }
44604
44682
  return {
44605
44683
  ...candidate,
44606
- desktopShot: desktop ? path32.relative(outDir, path32.join(dir, "desktop.png")) : null,
44684
+ desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
44607
44685
  mobileShot: null,
44608
- bundle: built ? path32.relative(outDir, path32.join(dir, "section.html")) : null,
44686
+ bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
44609
44687
  fidelity,
44610
44688
  ...fidelityNote ? { fidelityNote } : {},
44611
44689
  ...built ? { cssStats: built.stats } : {},
@@ -44624,9 +44702,9 @@ async function captureMobileShots(args) {
44624
44702
  for (const section of sections) {
44625
44703
  const shot = await captureSectionOnMobile(mobile.page, section);
44626
44704
  if (!shot) continue;
44627
- const file = path32.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
44628
- await writeFile16(file, shot);
44629
- section.mobileShot = path32.relative(outDir, file);
44705
+ const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
44706
+ await writeFile15(file, shot);
44707
+ section.mobileShot = path31.relative(outDir, file);
44630
44708
  }
44631
44709
  } finally {
44632
44710
  await mobile.context.close();
@@ -44641,10 +44719,10 @@ async function captureMotionTakes(args) {
44641
44719
  const filmOne = async (section) => {
44642
44720
  const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
44643
44721
  if (!take) return;
44644
- const dir = path32.join(sectionsDir, String(section.index).padStart(2, "0"));
44645
- const file = path32.join(dir, "motion-filmstrip.png");
44646
- await writeFile16(file, take.filmstrip);
44647
- section.motionFilmstrip = path32.relative(outDir, file);
44722
+ const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
44723
+ const file = path31.join(dir, "motion-filmstrip.png");
44724
+ await writeFile15(file, take.filmstrip);
44725
+ section.motionFilmstrip = path31.relative(outDir, file);
44648
44726
  log(` [${section.index}] ${section.motion.summary}`);
44649
44727
  };
44650
44728
  const queue = [...moving];
@@ -44701,7 +44779,7 @@ async function captureAlternateViews(args) {
44701
44779
  async function reproduceWholePage(args) {
44702
44780
  const { browser, page, outDir, pageUrl, withCode, log } = args;
44703
44781
  const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
44704
- if (fullPage) await writeFile16(path32.join(outDir, "full-page.png"), fullPage);
44782
+ if (fullPage) await writeFile15(path31.join(outDir, "full-page.png"), fullPage);
44705
44783
  if (!withCode) return { bundle: null, fidelity: null };
44706
44784
  const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
44707
44785
  log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
@@ -44772,7 +44850,7 @@ async function openViaLadder(args) {
44772
44850
  async function scrapeLanding(options) {
44773
44851
  const timeoutMs = options.timeoutMs ?? 45e3;
44774
44852
  const log = options.onProgress ?? (() => void 0);
44775
- const sectionsDir = path32.join(options.outDir, "sections");
44853
+ const sectionsDir = path31.join(options.outDir, "sections");
44776
44854
  const nonPublic = refuseNonPublicUrl(options.url);
44777
44855
  if (nonPublic) {
44778
44856
  throw new BlockedPageError({
@@ -44792,7 +44870,7 @@ async function scrapeLanding(options) {
44792
44870
  const withMotion = options.motion !== false && !escalated;
44793
44871
  const renderBrowser = options.code === false ? null : await launchBrowser();
44794
44872
  try {
44795
- await mkdir11(sectionsDir, { recursive: true });
44873
+ await mkdir10(sectionsDir, { recursive: true });
44796
44874
  const sections = await captureSections({
44797
44875
  browser: renderBrowser ?? browser,
44798
44876
  page,
@@ -44834,7 +44912,7 @@ async function scrapeLanding(options) {
44834
44912
  security: prepared.security,
44835
44913
  captureTier: tier
44836
44914
  };
44837
- await writeFile16(path32.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
44915
+ await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
44838
44916
  `);
44839
44917
  if (options.report !== false) {
44840
44918
  const reportPath = await writeCaptureReport(manifest, options.outDir);
@@ -44849,28 +44927,28 @@ async function scrapeLanding(options) {
44849
44927
 
44850
44928
  // src/commands/landing/inspiration/captureOut.ts
44851
44929
  import { existsSync as existsSync9 } from "fs";
44852
- import path33 from "path";
44930
+ import path32 from "path";
44853
44931
  var SCRATCH_DIR = ".baker";
44854
44932
  function isWithin(parent, target) {
44855
- const relative = path33.relative(parent, target);
44856
- return relative === "" || !relative.startsWith("..") && !path33.isAbsolute(relative);
44933
+ const relative = path32.relative(parent, target);
44934
+ return relative === "" || !relative.startsWith("..") && !path32.isAbsolute(relative);
44857
44935
  }
44858
44936
  function findRepoRoot(from) {
44859
- let dir = path33.resolve(from);
44937
+ let dir = path32.resolve(from);
44860
44938
  for (; ; ) {
44861
- if (existsSync9(path33.join(dir, ".git"))) return dir;
44862
- const parent = path33.dirname(dir);
44939
+ if (existsSync9(path32.join(dir, ".git"))) return dir;
44940
+ const parent = path32.dirname(dir);
44863
44941
  if (parent === dir) return null;
44864
44942
  dir = parent;
44865
44943
  }
44866
44944
  }
44867
44945
  function checkCaptureOut(out, options) {
44868
44946
  const { cwd, repoRoot } = options;
44869
- const resolved = path33.resolve(cwd, out);
44947
+ const resolved = path32.resolve(cwd, out);
44870
44948
  if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
44871
- const scratch = path33.join(repoRoot, SCRATCH_DIR);
44949
+ const scratch = path32.join(repoRoot, SCRATCH_DIR);
44872
44950
  if (isWithin(scratch, resolved)) return { ok: true };
44873
- const suggestion = path33.posix.join(SCRATCH_DIR, "teardowns", path33.basename(resolved) || "capture");
44951
+ const suggestion = path32.posix.join(SCRATCH_DIR, "teardowns", path32.basename(resolved) || "capture");
44874
44952
  return {
44875
44953
  ok: false,
44876
44954
  error: {
@@ -44893,29 +44971,8 @@ var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
44893
44971
  // would blacklist a page that was never actually judged.
44894
44972
  "PROXY_UNAVAILABLE"
44895
44973
  ]);
44896
- async function recordCapture(manifest, outDir) {
44897
- const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
44898
- const references = [];
44899
- for (const section of manifest.sections) {
44900
- if (!section.bundle) continue;
44901
- try {
44902
- const markup = await readFile23(path34.join(outDir, section.bundle), "utf8");
44903
- const copyStrings = capturedCopyStrings(markup);
44904
- if (copyStrings.length === 0) continue;
44905
- references.push({
44906
- sectionId: `local:${manifest.finalUrl}#${section.index}`,
44907
- sourceUrl: manifest.finalUrl,
44908
- copyStrings,
44909
- consultedAt
44910
- });
44911
- } catch {
44912
- }
44913
- }
44914
- const written = await recordReferences(process.cwd(), references);
44915
- return written ? references.length : 0;
44916
- }
44917
44974
  function captureHints(args) {
44918
- const { manifest, outDir, report, recorded } = args;
44975
+ const { manifest, outDir, report } = args;
44919
44976
  const shots = manifest.sections.filter((section) => section.desktopShot !== null).length;
44920
44977
  const anyScored = manifest.sections.some((section) => section.fidelity !== null);
44921
44978
  const hints = [];
@@ -44935,14 +44992,11 @@ function captureHints(args) {
44935
44992
  );
44936
44993
  }
44937
44994
  hints.push(INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt);
44938
- hints.push(
44939
- recorded > 0 ? `${recorded} sections are recorded for the originality check \u2014 \`baker landing critique\` will block a publish that ships their copy.` : "Nothing from this capture is recorded for the originality check, so it cannot see this page. Be especially careful not to reuse its copy."
44940
- );
44941
44995
  return hints;
44942
44996
  }
44943
44997
  registerSchema({
44944
44998
  command: "landing.inspiration.scrape",
44945
- description: "Start here when the user points at a specific page and wants it now: capture one landing page into a directory of section screenshots, standalone HTML bundles, a whole-page reproduction and motion filmstrips. Returns when the capture is done, unlike `add`. Read the screenshots. Also records every captured section for the originality check.",
44999
+ description: "Start here when the user points at a specific page and wants it now: capture one landing page into a directory of section screenshots, standalone HTML bundles, a whole-page reproduction and motion filmstrips. Returns when the capture is done, unlike `add`. Read the screenshots.",
44946
45000
  args: {
44947
45001
  url: { type: "string", description: "Page to capture", required: true },
44948
45002
  out: { type: "string", description: "Output directory under .baker/", required: true },
@@ -45010,7 +45064,6 @@ var scrapeCommand = defineCommand161({
45010
45064
  `)
45011
45065
  });
45012
45066
  const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
45013
- const recorded = await recordCapture(manifest, args.out);
45014
45067
  writeJson({
45015
45068
  ok: true,
45016
45069
  data: {
@@ -45031,7 +45084,7 @@ var scrapeCommand = defineCommand161({
45031
45084
  certificate_verified: false,
45032
45085
  ...manifest.security.certificateNotes.length > 0 ? { certificate_notes: manifest.security.certificateNotes } : {}
45033
45086
  },
45034
- hints: captureHints({ manifest, outDir: args.out, report: args.report, recorded })
45087
+ hints: captureHints({ manifest, outDir: args.out, report: args.report })
45035
45088
  });
45036
45089
  } catch (error) {
45037
45090
  const failure = classifyCaptureFailure(error);
@@ -45069,12 +45122,12 @@ var scrapeCommand = defineCommand161({
45069
45122
  });
45070
45123
 
45071
45124
  // src/commands/landing/inspiration/search.ts
45072
- import path36 from "path";
45125
+ import path34 from "path";
45073
45126
  import { defineCommand as defineCommand162 } from "citty";
45074
45127
 
45075
45128
  // src/commands/landing/inspiration/shot.ts
45076
- import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
45077
- import path35 from "path";
45129
+ import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
45130
+ import path33 from "path";
45078
45131
  import sharp6 from "sharp";
45079
45132
  var READABLE_SHOT = {
45080
45133
  maxWidth: 1440,
@@ -45100,9 +45153,9 @@ async function downloadReadableShot(url, file) {
45100
45153
  const response = await fetch(url);
45101
45154
  if (!response.ok) return null;
45102
45155
  const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
45103
- await mkdir12(path35.dirname(file), { recursive: true });
45104
- await writeFile17(file, shot);
45105
- return path35.relative(process.cwd(), file);
45156
+ await mkdir11(path33.dirname(file), { recursive: true });
45157
+ await writeFile16(file, shot);
45158
+ return path33.relative(process.cwd(), file);
45106
45159
  } catch {
45107
45160
  return null;
45108
45161
  }
@@ -45194,13 +45247,13 @@ function buildSearchBody(args) {
45194
45247
  return body;
45195
45248
  }
45196
45249
  async function downloadShots(results) {
45197
- const dir = path36.join(process.cwd(), ".baker", "inspiration");
45250
+ const dir = path34.join(process.cwd(), ".baker", "inspiration");
45198
45251
  const saved = /* @__PURE__ */ new Map();
45199
45252
  await Promise.all(
45200
45253
  results.map(async (result) => {
45201
45254
  const file = await downloadReadableShot(
45202
45255
  result.desktopShotUrl,
45203
- path36.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
45256
+ path34.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
45204
45257
  );
45205
45258
  if (file) saved.set(result.id, file);
45206
45259
  })
@@ -45428,7 +45481,7 @@ var sequencesCommand = defineCommand163({
45428
45481
  });
45429
45482
 
45430
45483
  // src/commands/landing/inspiration/view.ts
45431
- import path37 from "path";
45484
+ import path35 from "path";
45432
45485
  import { defineCommand as defineCommand164 } from "citty";
45433
45486
  registerSchema({
45434
45487
  command: "landing.inspiration.view",
@@ -45461,12 +45514,12 @@ var viewCommand2 = defineCommand164({
45461
45514
  const id = args.id;
45462
45515
  const data = await apiGet("/api/landing-inspiration/section", { id });
45463
45516
  const section = data.section;
45464
- const dir = path37.join(process.cwd(), ".baker", "inspiration", id);
45517
+ const dir = path35.join(process.cwd(), ".baker", "inspiration", id);
45465
45518
  const ext = READABLE_SHOT.extension;
45466
45519
  const [desktop, mobile, filmstrip] = await Promise.all([
45467
- downloadReadableShot(section.desktopShotUrl, path37.join(dir, `desktop.${ext}`)),
45468
- downloadReadableShot(section.mobileShotUrl, path37.join(dir, `mobile.${ext}`)),
45469
- downloadReadableShot(section.motionFilmstripUrl, path37.join(dir, `motion-filmstrip.${ext}`))
45520
+ downloadReadableShot(section.desktopShotUrl, path35.join(dir, `desktop.${ext}`)),
45521
+ downloadReadableShot(section.mobileShotUrl, path35.join(dir, `mobile.${ext}`)),
45522
+ downloadReadableShot(section.motionFilmstripUrl, path35.join(dir, `motion-filmstrip.${ext}`))
45470
45523
  ]);
45471
45524
  const full = args.full;
45472
45525
  const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
@@ -45533,7 +45586,7 @@ var inspirationCommand = defineCommand165({
45533
45586
 
45534
45587
  Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
45535
45588
 
45536
- This is inspiration, never a clipboard. Take the mechanism \u2014 what the eye hits first, what proof arrives before the ask, how the grid is split. The words are never yours to reuse: shipping a reference's headline is a Tier 0 message-match failure and \`baker landing critique\` will block the publish.
45589
+ This is inspiration, never a clipboard. Take the mechanism \u2014 what the eye hits first, what proof arrives before the ask, how the grid is split. The words are never yours to reuse: shipping a reference's headline is a Tier 0 message-match failure.
45537
45590
 
45538
45591
  Subcommands:
45539
45592
  baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
@@ -47315,8 +47368,8 @@ var listCommand15 = defineCommand183({
47315
47368
  });
47316
47369
 
47317
47370
  // src/commands/scheduled-actions/templates.ts
47318
- import { readFile as readFile24 } from "fs/promises";
47319
- import path38 from "path";
47371
+ import { readFile as readFile22 } from "fs/promises";
47372
+ import path36 from "path";
47320
47373
  import { defineCommand as defineCommand184 } from "citty";
47321
47374
  registerSchema({
47322
47375
  command: "scheduled-actions.templates",
@@ -47419,7 +47472,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
47419
47472
  }
47420
47473
  if (save.length > 0) {
47421
47474
  const briefFile = flag("brief-file");
47422
- const brief = briefFile.length > 0 ? await readFile24(path38.resolve(briefFile), "utf8") : flag("brief");
47475
+ const brief = briefFile.length > 0 ? await readFile22(path36.resolve(briefFile), "utf8") : flag("brief");
47423
47476
  if (brief.trim().length === 0) {
47424
47477
  failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
47425
47478
  }
@@ -47890,7 +47943,7 @@ function parseImageRefs(spec) {
47890
47943
  }
47891
47944
  var defaultDeps = {
47892
47945
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
47893
- upload: (path40) => uploadLocalImage({ file: path40, contentType: detectImageContentType(path40), source: "uploaded" })
47946
+ upload: (path38) => uploadLocalImage({ file: path38, contentType: detectImageContentType(path38), source: "uploaded" })
47894
47947
  };
47895
47948
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
47896
47949
  const refs = parseImageRefs(spec);
@@ -47910,10 +47963,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
47910
47963
  }
47911
47964
  return { imageIds, added };
47912
47965
  }
47913
- function uploadFailure(path40) {
47966
+ function uploadFailure(path38) {
47914
47967
  return (error) => {
47915
47968
  if (error instanceof ApiError) throw error;
47916
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path40}" as an image.`);
47969
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path38}" as an image.`);
47917
47970
  };
47918
47971
  }
47919
47972
 
@@ -48975,10 +49028,10 @@ async function stageOp4(op) {
48975
49028
  handleError5(err);
48976
49029
  }
48977
49030
  }
48978
- async function draftAction3(path40, body, chat) {
49031
+ async function draftAction3(path38, body, chat) {
48979
49032
  const chatId = resolveChatId(chat);
48980
49033
  try {
48981
- const data = await apiPost(path40, { chatId, ...body });
49034
+ const data = await apiPost(path38, { chatId, ...body });
48982
49035
  writeJsonEnvelope({ ok: true, data });
48983
49036
  return data;
48984
49037
  } catch (err) {
@@ -50085,7 +50138,7 @@ var groupCommand2 = defineCommand209({
50085
50138
  // src/commands/videos/ingest.ts
50086
50139
  import { mkdtemp as mkdtemp2, rm as rm7, stat as stat7 } from "fs/promises";
50087
50140
  import { tmpdir as tmpdir3 } from "os";
50088
- import path39 from "path";
50141
+ import path37 from "path";
50089
50142
  import { defineCommand as defineCommand210 } from "citty";
50090
50143
 
50091
50144
  // src/lib/streamUpload.ts
@@ -50436,7 +50489,7 @@ function ingestUrl(args) {
50436
50489
  }
50437
50490
  async function downloadThenIngest(args, country) {
50438
50491
  const vimeoCookie = captureVimeoCookie();
50439
- const workDir = await mkdtemp2(path39.join(tmpdir3(), "videos-ingest-"));
50492
+ const workDir = await mkdtemp2(path37.join(tmpdir3(), "videos-ingest-"));
50440
50493
  try {
50441
50494
  const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
50442
50495
  if (isAudioOnly(probe.info)) {
@@ -50572,7 +50625,7 @@ var searchCommand4 = defineCommand211({
50572
50625
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
50573
50626
 
50574
50627
  // src/commands/videos/upload.ts
50575
- import { readFile as readFile25, stat as stat8 } from "fs/promises";
50628
+ import { readFile as readFile23, stat as stat8 } from "fs/promises";
50576
50629
  import { basename as basename3, extname as extname4 } from "path";
50577
50630
  import { defineCommand as defineCommand212 } from "citty";
50578
50631
  var MIME_MAP = {
@@ -50669,7 +50722,7 @@ var uploadCommand2 = defineCommand212({
50669
50722
  originalFilename,
50670
50723
  descriptionContext
50671
50724
  });
50672
- const fileBuffer = await readFile25(filePath);
50725
+ const fileBuffer = await readFile23(filePath);
50673
50726
  const uploadResponse = await fetch(uploadUrl, {
50674
50727
  method: "PUT",
50675
50728
  headers: { "Content-Type": contentType },
@@ -52056,7 +52109,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
52056
52109
  };
52057
52110
  }
52058
52111
  function commandPathOf(root, argv) {
52059
- const path40 = [];
52112
+ const path38 = [];
52060
52113
  let command = root;
52061
52114
  for (const token of argv) {
52062
52115
  if (token === "--" || token.startsWith("-")) {
@@ -52067,10 +52120,10 @@ function commandPathOf(root, argv) {
52067
52120
  if (next === void 0 || typeof next !== "object") {
52068
52121
  break;
52069
52122
  }
52070
- path40.push(token);
52123
+ path38.push(token);
52071
52124
  command = next;
52072
52125
  }
52073
- return path40.join(" ");
52126
+ return path38.join(" ");
52074
52127
  }
52075
52128
  function refuseUnknownFlags(root, argv) {
52076
52129
  const unknown = findUnknownFlags(root, argv);