@koda-sl/baker-cli 0.354.0-dev.faae9de67 → 0.355.0-dev.c2de9a6fe

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/README.md CHANGED
@@ -742,7 +742,7 @@ Use `--no-cache` on any command to bypass **both** the local file cache and the
742
742
  |------|-----------|---------|
743
743
  | `FIELD_NOT_FOUND` | No | Invalid field in GAQL query |
744
744
  | `WRONG_RESOURCE` | No | Field queried from wrong resource |
745
- | `INVALID_OPERATOR` | No | Unsupported operator (e.g. CONTAINS) |
745
+ | `INVALID_OPERATOR` | No | Unsupported operator (e.g. CONTAINS(), or =, IN or IS NOT NULL on a list field such as `final_urls` or `labels`, which takes only CONTAINS ALL/ANY/NONE). List-field operators are refused before the query is sent, with a corrected command that rewrites =/IN to CONTAINS ANY and drops a null test |
746
746
  | `CUSTOMER_NOT_FOUND` | No | Invalid customer ID — run `baker ads google accounts` to find valid IDs |
747
747
  | `MISSING_MANAGER_ID` | No | Account not accessible (may need MCC login-customer-id) |
748
748
  | `INCOMPATIBLE_FIELDS` | No | Fields can't be in same query |
@@ -1421,8 +1421,8 @@ baker experiment plan --landing oferta --variant oferta--1 --goal submit:book-a-
1421
1421
  baker experiment start --landing oferta --variant oferta--1 --because "60% of visitors never scroll past the hero" --change "put the price in the headline"
1422
1422
  baker experiment update --id temp_exp_1 --goal submit:book-a-call # a staged test can still change what it measures
1423
1423
  baker experiment update --id a3f91c2b --because "…" # a live one can only change its wording
1424
- baker experiment status # the verdict on every test, first; dates as calendar dates
1425
- baker experiment status --id a3f91c2b --full # one test, with everything the dashboard reads and the posteriors
1424
+ baker experiment status # the verdict on every test
1425
+ baker experiment status --id a3f91c2b --full # one test, with the confidence and posteriors behind it
1426
1426
  baker experiment history --landing oferta # every finished test on a page: claim, numbers, decision, learning
1427
1427
  baker experiment pause --id a3f91c2b # everyone sees the original for now
1428
1428
  baker experiment resume --id a3f91c2b # the split picks up with the same sides
@@ -5412,7 +5412,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5412
5412
 
5413
5413
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5414
5414
 
5415
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5415
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in — only on a video with nobody in it: when the deconstruct found a cast or a recurring person, reuse is refused (`frames_reuse_refused`) and every frame is generated, so the source's actors never reach the ad.
5416
5416
 
5417
5417
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5418
5418
 
@@ -5466,7 +5466,7 @@ baker canvas run ./reference-ad.video.canvas.json
5466
5466
  |---|---|---|
5467
5467
  | `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
5468
5468
  | `--slug <slug>` | — | Creative slug (lowercase kebab): writes the canvas to `src/creatives/<slug>/<slug>.canvas.json` — the repo convention that attaches every run to the creative's dashboard generation history. `--out` wins over `--slug`. |
5469
- | `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast). |
5469
+ | `--frames <mode>` | `generate` | `generate` emits ONE recast keyframe per scene (the original frame is dropped so the dropped `el_*` assets drive identity); `reuse` wires the real extracted first+last frames straight into the clips (faithful, cheaper, no recast) — only on a video with nobody in it; on one showing people it is refused and every frame is generated. |
5470
5470
  | `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
5471
5471
  | `--seam-dedup <mode>` | `head` | How to dedup the boundary frame two clips SHARE when a long shot is split for length (the second clip's first frame IS the first clip's last frame, so a plain concat freezes on it for a frame). `head` drops the second clip's first frame, `tail` drops the first clip's last frame, `off` keeps both. Only touches shared-frame continuation joins — a hard cut between two shots shares no frame. |
5472
5472
  | `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
package/dist/cli.js CHANGED
@@ -10564,6 +10564,102 @@ function hintsFor(scope, days) {
10564
10564
  return hints2;
10565
10565
  }
10566
10566
 
10567
+ // src/commands/ads/google/list-fields.ts
10568
+ var LIST_FIELDS = /* @__PURE__ */ new Set([
10569
+ "ad.final_mobile_urls",
10570
+ "ad.final_urls",
10571
+ "ad_group.excluded_parent_asset_field_types",
10572
+ "ad_group.excluded_parent_asset_set_types",
10573
+ "ad_group.labels",
10574
+ "ad_group.primary_status_reasons",
10575
+ "ad_group_ad.ad.final_mobile_urls",
10576
+ "ad_group_ad.ad.final_urls",
10577
+ "ad_group_ad.labels",
10578
+ "ad_group_ad.primary_status_reasons",
10579
+ "ad_group_asset.primary_status_reasons",
10580
+ "ad_group_criterion.disapproval_reasons",
10581
+ "ad_group_criterion.final_mobile_urls",
10582
+ "ad_group_criterion.final_urls",
10583
+ "ad_group_criterion.labels",
10584
+ "ad_group_criterion.primary_status_reasons",
10585
+ "ad_group_criterion.topic.path",
10586
+ "ad_group_criterion.webpage.sample.sample_urls",
10587
+ "asset.dynamic_custom_asset.contextual_keywords",
10588
+ "asset.dynamic_custom_asset.similar_ids",
10589
+ "asset.dynamic_education_asset.contextual_keywords",
10590
+ "asset.dynamic_education_asset.similar_program_ids",
10591
+ "asset.dynamic_flights_asset.similar_destination_ids",
10592
+ "asset.dynamic_hotels_and_rentals_asset.contextual_keywords",
10593
+ "asset.dynamic_hotels_and_rentals_asset.similar_property_ids",
10594
+ "asset.dynamic_jobs_asset.contextual_keywords",
10595
+ "asset.dynamic_jobs_asset.similar_job_ids",
10596
+ "asset.dynamic_local_asset.contextual_keywords",
10597
+ "asset.dynamic_local_asset.similar_deal_ids",
10598
+ "asset.dynamic_real_estate_asset.contextual_keywords",
10599
+ "asset.dynamic_real_estate_asset.similar_listing_ids",
10600
+ "asset.dynamic_travel_asset.contextual_keywords",
10601
+ "asset.dynamic_travel_asset.similar_destination_ids",
10602
+ "asset.final_mobile_urls",
10603
+ "asset.final_urls",
10604
+ "asset.page_feed_asset.labels",
10605
+ "asset.structured_snippet_asset.values",
10606
+ "asset_group.final_mobile_urls",
10607
+ "asset_group.final_urls",
10608
+ "asset_group.primary_status_reasons",
10609
+ "asset_group_asset.primary_status_reasons",
10610
+ "asset_group_signal.disapproval_reasons",
10611
+ "asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.label_filters",
10612
+ "asset_set.business_profile_location_group.dynamic_business_profile_location_group_filter.listing_id_filters",
10613
+ "asset_set.location_set.business_profile_location_set.label_filters",
10614
+ "asset_set.location_set.business_profile_location_set.listing_id_filters",
10615
+ "bidding_data_exclusion.advertising_channel_types",
10616
+ "bidding_data_exclusion.campaigns",
10617
+ "bidding_data_exclusion.devices",
10618
+ "bidding_seasonality_adjustment.advertising_channel_types",
10619
+ "bidding_seasonality_adjustment.campaigns",
10620
+ "bidding_seasonality_adjustment.devices",
10621
+ "campaign.excluded_parent_asset_field_types",
10622
+ "campaign.excluded_parent_asset_set_types",
10623
+ "campaign.feed_types",
10624
+ "campaign.labels",
10625
+ "campaign.optimization_goal_setting.optimization_goal_types",
10626
+ "campaign.primary_status_reasons",
10627
+ "campaign.selective_optimization.conversion_actions",
10628
+ "campaign.shopping_setting.advertising_partner_ids",
10629
+ "campaign.text_guidelines.term_exclusions",
10630
+ "campaign_asset.primary_status_reasons",
10631
+ "campaign_criterion.location_group.geo_target_constants",
10632
+ "campaign_criterion.topic.path",
10633
+ "campaign_criterion.webpage.sample.sample_urls",
10634
+ "conversion_value_rule.audience_condition.user_interests",
10635
+ "conversion_value_rule.audience_condition.user_lists",
10636
+ "conversion_value_rule.device_condition.device_types",
10637
+ "conversion_value_rule.geo_location_condition.excluded_geo_target_constants",
10638
+ "conversion_value_rule.geo_location_condition.geo_target_constants",
10639
+ "conversion_value_rule_set.conversion_value_rules",
10640
+ "conversion_value_rule_set.dimensions",
10641
+ "custom_conversion_goal.conversion_actions",
10642
+ "customer.pay_per_conversion_eligibility_failure_reasons",
10643
+ "customer_asset.primary_status_reasons",
10644
+ "customer_client.applied_labels",
10645
+ "experiment_arm.campaigns",
10646
+ "experiment_arm.in_design_campaigns",
10647
+ "keyword_plan_campaign.language_constants",
10648
+ "local_services_employee.category_ids",
10649
+ "local_services_employee.languages_spoken",
10650
+ "metrics.interaction_event_types",
10651
+ "recommendation.campaigns",
10652
+ "shared_criterion.webpage.sample.sample_urls",
10653
+ "shopping_product.target_countries",
10654
+ "topic_constant.path",
10655
+ "user_list.lookalike_user_list.country_codes",
10656
+ "user_list.lookalike_user_list.seed_user_list_ids"
10657
+ ]);
10658
+ var LIST_FIELD_OPERATORS = "CONTAINS ALL, CONTAINS ANY, CONTAINS NONE";
10659
+ function listFieldGuidance(field) {
10660
+ return `${field} is a list field: it takes only ${LIST_FIELD_OPERATORS} \u2014 no comparison (=, !=, IN), pattern match or null test. To find rows that have it, drop that condition, keep ${field} in SELECT and filter the rows (rows without it omit the key); to match values, use ${field} CONTAINS ANY ('value')`;
10661
+ }
10662
+
10567
10663
  // src/commands/ads/google/correction-table.ts
10568
10664
  function buildCommand(query, ctx) {
10569
10665
  return `baker ads google query "${query}" --customer-id ${ctx.customerId}`;
@@ -10591,6 +10687,27 @@ var CORRECTION_RULES = [
10591
10687
  };
10592
10688
  }
10593
10689
  },
10690
+ // 0b. OPERATOR_FIELD_MISMATCH — the field refuses this operator, and Google names the
10691
+ // ones it takes. Kept ahead of the field-specific rules for the same reason as rule 0.
10692
+ // A repeated field (final_urls, labels, …) takes only CONTAINS ALL/ANY/NONE, so it has
10693
+ // no null test at all: read it unfiltered and filter the rows instead.
10694
+ {
10695
+ matchApiError: /OPERATOR_FIELD_MISMATCH|is not a valid operator to use with/i,
10696
+ fix: (ctx) => {
10697
+ const message = ctx.apiErrorMessage ?? "";
10698
+ const field = message.match(/valid operator to use with '([\w.]+)'/i)?.[1] ?? "this field";
10699
+ const operator = message.match(/([A-Z=!<>][A-Z_ =!<>]*?) is not a valid operator/)?.[1]?.trim();
10700
+ const valid = message.match(/Valid operators: (.+?)\.?(?:"|$)/i)?.[1]?.trim();
10701
+ const refused = operator ? `${operator} cannot be used on ${field}` : `${field} does not take that operator`;
10702
+ const allowed = valid ? `; its valid operators are ${valid}` : "";
10703
+ const repeated = valid?.split(",").every((op) => /^CONTAINS (ALL|ANY|NONE)$/i.test(op.trim())) ?? false;
10704
+ const guidance = repeated ? `. ${listFieldGuidance(field)}` : ". Rewrite that condition with one of them, or drop it and filter the rows";
10705
+ return {
10706
+ action: "change_operator",
10707
+ explanation: `${refused}${allowed}${guidance}`
10708
+ };
10709
+ }
10710
+ },
10594
10711
  // 1. bare keyword.text → ad_group_criterion.keyword.text (leaves <resource>.keyword.text untouched)
10595
10712
  {
10596
10713
  matchQuery: /(?<![\w.])keyword\.text\b/,
@@ -10633,10 +10750,11 @@ var CORRECTION_RULES = [
10633
10750
  };
10634
10751
  }
10635
10752
  },
10636
- // 4. CONTAINS → LIKE
10753
+ // 4. CONTAINS → LIKE. Keyed on the query alone: Google's error envelope ("Request contains an
10754
+ // invalid argument … Valid operators: CONTAINS ANY") matches any loose error pattern, and this
10755
+ // advice only makes sense when the query actually wrote CONTAINS(…).
10637
10756
  {
10638
10757
  matchQuery: /CONTAINS\s*\(/i,
10639
- matchApiError: /CONTAINS.*not.*supported|invalid.*operator.*CONTAINS/i,
10640
10758
  fix: (ctx) => {
10641
10759
  const match = ctx.originalQuery.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
10642
10760
  if (match) {
@@ -13082,6 +13200,73 @@ function rejectOversizedLimit(query, customerId) {
13082
13200
  }
13083
13201
  };
13084
13202
  }
13203
+ var WHERE_OPERATOR = /(?<![\w.])([a-z_]+(?:\.[a-z_]+)+)\s*(CONTAINS\s+(?:ALL|ANY|NONE)|IS\s+NOT\s+NULL|IS\s+NULL|NOT\s+LIKE|NOT\s+IN|NOT\s+REGEXP_MATCH|REGEXP_MATCH|LIKE|IN|BETWEEN|DURING|!=|<=|>=|=|<|>)/gi;
13204
+ function escapeRegExp(text2) {
13205
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13206
+ }
13207
+ function withoutNullTest(query, field) {
13208
+ const test = `${escapeRegExp(field)}\\s+IS\\s+(?:NOT\\s+)?NULL\\b`;
13209
+ const dropped = query.replace(new RegExp(`\\bWHERE\\s+${test}\\s+AND\\s+`, "i"), "WHERE ").replace(new RegExp(`\\s+AND\\s+${test}`, "i"), "").replace(new RegExp(`\\s*\\bWHERE\\s+${test}`, "i"), "");
13210
+ return mentions(selectClause(dropped), field) ? dropped : dropped.replace(/SELECT\s+/i, `SELECT ${field}, `);
13211
+ }
13212
+ function asListCondition(query, field) {
13213
+ const condition = new RegExp(
13214
+ `(?<![\\w.])${escapeRegExp(field)}\\s*(NOT\\s+IN\\b|IN\\b|!=|=)\\s*('[^']*'|\\([^)]*\\))`,
13215
+ "i"
13216
+ );
13217
+ const match = query.match(condition);
13218
+ if (!match) return null;
13219
+ const [, operator = "", value = ""] = match;
13220
+ const contains = /^(=|IN)$/i.test(operator.trim()) ? "CONTAINS ANY" : "CONTAINS NONE";
13221
+ const values = value.startsWith("(") ? value : `(${value})`;
13222
+ return query.replace(condition, `${field} ${contains} ${values}`);
13223
+ }
13224
+ function rejectListFieldOperator(query, customerId) {
13225
+ const where = whereClause(query).replace(/'[^']*'/g, "''");
13226
+ const refusals = [...where.matchAll(WHERE_OPERATOR)].map(([, field = "", operator = ""]) => ({
13227
+ field: field.toLowerCase(),
13228
+ operator: operator.toUpperCase().replace(/\s+/g, " ")
13229
+ })).filter(({ field, operator }) => LIST_FIELDS.has(field) && !operator.startsWith("CONTAINS"));
13230
+ const first = refusals[0];
13231
+ if (!first) return null;
13232
+ const fixed = refusals.reduce(
13233
+ (corrected, { field, operator }) => corrected === null ? null : operator.startsWith("IS ") ? withoutNullTest(corrected, field) : asListCondition(corrected, field),
13234
+ query
13235
+ );
13236
+ return {
13237
+ ok: false,
13238
+ error: {
13239
+ code: "INVALID_OPERATOR",
13240
+ message: `${first.operator} cannot be used on ${first.field}: it is a list field, so Google would refuse this query.`,
13241
+ fix: {
13242
+ action: "change_operator",
13243
+ ...fixed ? { correctedCommand: buildCommand2(fixed, customerId) } : {},
13244
+ explanation: listFieldGuidance(first.field)
13245
+ },
13246
+ retryable: false
13247
+ }
13248
+ };
13249
+ }
13250
+ function rejectContainsCall(query, customerId) {
13251
+ const containsMatch = query.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
13252
+ if (!containsMatch) return null;
13253
+ const field = containsMatch[1]?.trim();
13254
+ const value = containsMatch[2] ?? "";
13255
+ const fixed = query.replace(/CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i, `${field} LIKE '%${value}%'`);
13256
+ return {
13257
+ ok: false,
13258
+ error: {
13259
+ code: "INVALID_OPERATOR",
13260
+ message: "GAQL does not support CONTAINS. Use LIKE with % wildcards.",
13261
+ fix: {
13262
+ action: "change_operator",
13263
+ correctedCommand: buildCommand2(fixed, customerId),
13264
+ explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
13265
+ },
13266
+ retryable: false
13267
+ }
13268
+ };
13269
+ }
13085
13270
  function validatePreflight(query, customerId, limit) {
13086
13271
  const warnings = [];
13087
13272
  const corrected = query;
@@ -13104,29 +13289,6 @@ function validatePreflight(query, customerId, limit) {
13104
13289
  }
13105
13290
  };
13106
13291
  }
13107
- const containsMatch = corrected.match(/CONTAINS\s*\(\s*([^,]+),\s*'([^']+)'\s*\)/i);
13108
- if (containsMatch) {
13109
- const field = containsMatch[1]?.trim();
13110
- const value = containsMatch[2] ?? "";
13111
- const fixed = corrected.replace(/CONTAINS\s*\(\s*[^,]+,\s*'[^']+'\s*\)/i, `${field} LIKE '%${value}%'`);
13112
- return {
13113
- valid: false,
13114
- warnings: [],
13115
- error: {
13116
- ok: false,
13117
- error: {
13118
- code: "INVALID_OPERATOR",
13119
- message: "GAQL does not support CONTAINS. Use LIKE with % wildcards.",
13120
- fix: {
13121
- action: "change_operator",
13122
- correctedCommand: buildCommand2(fixed, customerId),
13123
- explanation: "GAQL uses LIKE '%value%' for substring matching, not CONTAINS()"
13124
- },
13125
- retryable: false
13126
- }
13127
- }
13128
- };
13129
- }
13130
13292
  const openDateMatch = corrected.match(/segments\.date\s*>=\s*'(\d{4}-\d{2}-\d{2})'/i);
13131
13293
  if (openDateMatch && !/BETWEEN/i.test(corrected)) {
13132
13294
  const startDate = openDateMatch[1] ?? "";
@@ -13153,10 +13315,10 @@ function validatePreflight(query, customerId, limit) {
13153
13315
  }
13154
13316
  };
13155
13317
  }
13156
- const duringRejection = rejectUnsupportedDuring(corrected, customerId);
13157
- if (duringRejection) return { valid: false, warnings: [], error: duringRejection };
13158
- const limitRejection = rejectOversizedLimit(corrected, customerId);
13159
- if (limitRejection) return { valid: false, warnings: [], error: limitRejection };
13318
+ for (const reject of [rejectContainsCall, rejectListFieldOperator, rejectUnsupportedDuring, rejectOversizedLimit]) {
13319
+ const rejection2 = reject(corrected, customerId);
13320
+ if (rejection2) return { valid: false, warnings: [], error: rejection2 };
13321
+ }
13160
13322
  if (/\bsegments\.conversion_action_name\b/i.test(corrected) && /\bmetrics\.cost_micros\b/i.test(corrected)) {
13161
13323
  return {
13162
13324
  valid: false,
@@ -39734,10 +39896,18 @@ function dropUncastVoices(nodes) {
39734
39896
  if (node && node.type === "voice_select" && !referenced.has(node.id)) nodes.splice(i, 1);
39735
39897
  }
39736
39898
  }
39737
- function scaffoldVideoCanvas(input, elementsInput, opts) {
39899
+ function framesReuseRefusal(blueprint, elements, requested) {
39900
+ if (requested !== "reuse") return void 0;
39901
+ const showsPeople = (blueprint.global?.cast ?? []).length > 0 || elements.some((e) => e.type.toLowerCase() === "person");
39902
+ if (!showsPeople) return void 0;
39903
+ return "--frames reuse was not applied: this video shows people, and reusing its frames would put the source's actors into our ad. Every frame is generated instead, recast to the el_* references. Reuse applies only to a video with nobody in it.";
39904
+ }
39905
+ function scaffoldVideoCanvas(input, elementsInput, requested) {
39738
39906
  const blueprint = VideoBlueprint.parse(input);
39739
39907
  injectHookPhysicality(blueprint);
39740
39908
  const elements = RecurringElements.parse(elementsInput);
39909
+ const reuseRefused = framesReuseRefusal(blueprint, elements, requested.frames);
39910
+ const opts = reuseRefused ? { ...requested, frames: "generate" } : requested;
39741
39911
  const clock = spineClockFor(blueprint);
39742
39912
  const nodes = [];
39743
39913
  emitBlueprintIngests(opts, nodes);
@@ -39844,6 +40014,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
39844
40014
  }
39845
40015
  const todo = {
39846
40016
  ...buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts, blueprint),
40017
+ ...reuseRefused ? { frames_reuse_refused: reuseRefused } : {},
39847
40018
  ...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel))),
39848
40019
  model_constraints: buildModelConstraints()
39849
40020
  };
@@ -44651,6 +44822,12 @@ function videoDefinitionDescription(blueprint) {
44651
44822
  const product = g.branding?.product;
44652
44823
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
44653
44824
  }
44825
+ function framesModeReport(canvas, requested) {
44826
+ const todo = canvas.metadata?.todo;
44827
+ const mode = typeof todo?.frames_mode === "string" ? todo.frames_mode : requested;
44828
+ const refused = typeof todo?.frames_reuse_refused === "string" ? todo.frames_reuse_refused : void 0;
44829
+ return { frames_mode: mode, ...refused ? { frames_reuse_refused: refused } : {} };
44830
+ }
44654
44831
  async function materializeReferenceVideo(fileArg2) {
44655
44832
  if (!/^https?:\/\//i.test(fileArg2)) return path26.resolve(fileArg2);
44656
44833
  let bytes;
@@ -44808,7 +44985,10 @@ var scaffoldVideoCommand = defineCommand116({
44808
44985
  type: "string",
44809
44986
  description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
44810
44987
  },
44811
- frames: { type: "string", description: '"generate" (default, anchored regen) or "reuse" (wire real frames in)' },
44988
+ frames: {
44989
+ type: "string",
44990
+ description: `"generate" (default, every frame regenerated for our ad) or "reuse" (wire the source's real frames in). Reuse applies only to a video with nobody in it \u2014 on a video showing people it is refused and every frame is generated, so the source's actors never reach our ad.`
44991
+ },
44812
44992
  ambient: {
44813
44993
  type: "boolean",
44814
44994
  description: "Give silent b-roll scenes native diegetic ambient mixed deep under the music bed (off by default)"
@@ -44996,6 +45176,7 @@ var scaffoldVideoCommand = defineCommand116({
44996
45176
  error: { code: "validation", issues: validation.issues },
44997
45177
  canvas_path: outPath,
44998
45178
  prompt_path: blueprintPath,
45179
+ ...framesModeReport(canvas, frames),
44999
45180
  note: "The canvas WAS written despite the blocking issue(s) \u2014 fix them in place, then `baker canvas validate` before running."
45000
45181
  },
45001
45182
  null,
@@ -45045,7 +45226,7 @@ var scaffoldVideoCommand = defineCommand116({
45045
45226
  source_reference: sourceRef.url,
45046
45227
  composition_dir: compositionDest,
45047
45228
  output: canvas.output,
45048
- frames_mode: frames,
45229
+ ...framesModeReport(canvas, frames),
45049
45230
  models: {
45050
45231
  deconstruct: deconstructModel,
45051
45232
  select: selectModel,
@@ -47115,80 +47296,6 @@ async function declarationsOf(root, files) {
47115
47296
  return sources.flatMap(eventDeclarationsIn);
47116
47297
  }
47117
47298
 
47118
- // src/commands/experiment/readout.ts
47119
- function isoMoment(ms) {
47120
- return ms === null ? null : new Date(ms).toISOString();
47121
- }
47122
- function isoDay2(ms) {
47123
- return ms === null ? null : new Date(ms).toISOString().slice(0, 10);
47124
- }
47125
- function compactArm(arm) {
47126
- return { visitors: arm.visitors, conversions: arm.conversions, rate: Math.round(arm.rate * 1e4) / 1e4 };
47127
- }
47128
- function compactMetric(metric) {
47129
- return {
47130
- name: metric.name,
47131
- reading: metric.reading,
47132
- control: metric.control.conversions,
47133
- variant: metric.variant.conversions
47134
- };
47135
- }
47136
- function fullArm(arm) {
47137
- return {
47138
- ...arm,
47139
- ...arm.snapshotCapturedAt === void 0 ? {} : { snapshotCapturedAt: isoMoment(arm.snapshotCapturedAt) }
47140
- };
47141
- }
47142
- function statusReadout(row, { full, now }) {
47143
- const decisionWhen = {
47144
- earliestDecisionAt: isoDay2(row.earliestDecisionAt),
47145
- ...row.earliestDecisionAt === null ? {} : { earliestDecisionIn: describeWait(row.earliestDecisionAt, now) }
47146
- };
47147
- const moments = {
47148
- startedAt: isoMoment(row.startedAt),
47149
- pausedAt: isoMoment(row.pausedAt),
47150
- finishedAt: isoMoment(row.finishedAt)
47151
- };
47152
- if (full) {
47153
- return {
47154
- ...row,
47155
- ...moments,
47156
- ...decisionWhen,
47157
- control: fullArm(row.control),
47158
- variant: fullArm(row.variant)
47159
- };
47160
- }
47161
- return {
47162
- experimentId: row.experimentId,
47163
- landingSlug: row.landingSlug,
47164
- variantSlug: row.variantSlug,
47165
- status: row.status,
47166
- verdict: row.verdict,
47167
- summary: row.summary,
47168
- ...row.winner === null ? {} : { winner: row.winner },
47169
- ...row.invalidReason === null ? {} : { invalidReason: row.invalidReason },
47170
- stillNeeded: row.stillNeeded,
47171
- ...decisionWhen,
47172
- willNotConclude: row.willNotConclude,
47173
- control: compactArm(row.control),
47174
- variant: compactArm(row.variant),
47175
- secondary: row.secondary.map(compactMetric),
47176
- hypothesisStatement: row.hypothesisStatement,
47177
- minDetectableRelativeLift: row.minDetectableRelativeLift,
47178
- goal: row.goal,
47179
- ...row.variantGoal === null ? {} : { variantGoal: row.variantGoal },
47180
- goalLabel: row.goalLabel,
47181
- ...moments,
47182
- ...row.status === "finished" ? {
47183
- survivor: row.survivor,
47184
- hypothesisOutcome: row.hypothesisOutcome,
47185
- fold: row.fold,
47186
- learning: row.learning,
47187
- endReason: row.endReason
47188
- } : {}
47189
- };
47190
- }
47191
-
47192
47299
  // src/commands/experiment/update.ts
47193
47300
  function buildUpdateRequest(args5, rawArgs) {
47194
47301
  const expect = args5.expect === void 0 ? void 0 : String(args5.expect);
@@ -47430,26 +47537,18 @@ registerSchema({
47430
47537
  args: {
47431
47538
  id: { type: "string", description: "One test, by its id", required: false },
47432
47539
  landing: LANDING_FILTER_ARG,
47433
- full: {
47434
- type: "boolean",
47435
- description: "Every field the dashboard reads too \u2014 the hypothesis in parts, what each version owns, the preview pictures and the posteriors behind the verdict",
47436
- required: false
47437
- }
47540
+ full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
47438
47541
  }
47439
47542
  });
47440
47543
  var statusCommand6 = defineCommand124({
47441
47544
  meta: {
47442
47545
  name: "status",
47443
- description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | cannot_win | invalid. `cannot_win` means even the best case is under the lift the test was looking for \u2014 end it and keep the original. `stillNeeded` is the data (visitors, conversions) the test still needs; a test is gated on data, never on days. `earliestDecisionAt` is the day this page's traffic would get it there (YYYY-MM-DD) and `earliestDecisionIn` how far off that is \u2014 say those, never work a date out yourself. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
47546
+ description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | cannot_win | invalid. `cannot_win` means even the best case is under the lift the test was looking for \u2014 end it and keep the original. `stillNeeded` is the data (visitors, conversions) the test still needs; a test is gated on data, never on days. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
47444
47547
  },
47445
47548
  args: {
47446
47549
  id: { type: "string", description: "One test, by its id", required: false },
47447
47550
  landing: LANDING_FILTER_ARG,
47448
- full: {
47449
- type: "boolean",
47450
- description: "Every field the dashboard reads too \u2014 the hypothesis in parts, what each version owns, the preview pictures and the posteriors behind the verdict",
47451
- required: false
47452
- }
47551
+ full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
47453
47552
  },
47454
47553
  run: async ({ args: args5 }) => {
47455
47554
  try {
@@ -47458,13 +47557,10 @@ var statusCommand6 = defineCommand124({
47458
47557
  ...args5.landing ? { landingSlug: String(args5.landing) } : {},
47459
47558
  ...args5.full === true ? { full: true } : {}
47460
47559
  });
47461
- const now = Date.now();
47462
47560
  writeJsonEnvelope({
47463
47561
  ok: true,
47464
- data: {
47465
- experiments: response.data.experiments.map((row) => statusReadout(row, { full: args5.full === true, now }))
47466
- },
47467
- hints: buildStatusHints(response.data.experiments, now)
47562
+ data: response.data,
47563
+ hints: buildStatusHints(response.data.experiments)
47468
47564
  });
47469
47565
  } catch (error) {
47470
47566
  reportApiError(error);
@@ -57608,7 +57704,7 @@ function mentionsInSource(source, names) {
57608
57704
  const out = [];
57609
57705
  for (const name of names) {
57610
57706
  const seen = /* @__PURE__ */ new Set();
57611
- for (const m of visible.matchAll(new RegExp(`\\b${escapeRegExp(name)}\\b`, "gi"))) {
57707
+ for (const m of visible.matchAll(new RegExp(`\\b${escapeRegExp2(name)}\\b`, "gi"))) {
57612
57708
  const at = m.index ?? 0;
57613
57709
  if (tables.some(([from, to]) => at >= from && at < to)) continue;
57614
57710
  const start = sentenceStart(visible, at);
@@ -57637,7 +57733,7 @@ function blankHtmlKeepingOffsets(text2) {
57637
57733
  }
57638
57734
  return out.join("");
57639
57735
  }
57640
- function escapeRegExp(value) {
57736
+ function escapeRegExp2(value) {
57641
57737
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57642
57738
  }
57643
57739
  function detectSource(source) {