@koda-sl/baker-cli 0.114.0-dev.249eaa8ed → 0.114.1

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
@@ -653,50 +653,6 @@ baker ads google library search-competitors "crm software" --location uk
653
653
 
654
654
  ---
655
655
 
656
- ### Staged writes (`baker ads google budgets|campaigns|...`)
657
-
658
- Write commands **never touch the Google Ads API at stage time**. Each command stages a create/update/pause/resume/remove op against the current chat's draft (`BAKER_CHAT_ID`); the dashboard shows it as a pending "Google Ads" change, and the whole draft applies as one atomic `GoogleAdsService.Mutate` when the chat is published. Feature-flagged per company (`companies.googleAdsWriteEnabled`) — off by default = a fully simulated publish with zero real API calls.
659
-
660
- Reference not-yet-created resources with `g_temp_*` refs returned by earlier `create` commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
661
-
662
- ```bash
663
- # Build a Search campaign bottom-up, chaining temp refs
664
- baker ads google budgets create --customer-id 1234567890 --name "Search budget" --amount 50
665
- # → { ref: "g_temp_ab12", ... }
666
- baker ads google campaigns create --customer-id 1234567890 --name "Brand — Search" \
667
- --channel-type SEARCH --budget-ref g_temp_ab12 --bidding-strategy MANUAL_CPC
668
- baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
669
- baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
670
- # Shared negative keyword list → attach to the campaign
671
- baker ads google keyword-lists create --customer-id 1234567890 --name "Brand exclusions"
672
- baker ads google keyword-lists add --customer-id 1234567890 --list-ref g_temp_<list> --text "free" --match-type BROAD
673
- baker ads google keyword-lists attach --customer-id 1234567890 --campaign-ref g_temp_<campaign> --list-ref g_temp_<list>
674
-
675
- # Batch keywords — one command stages the whole set as ONE request (up to 500)
676
- baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
677
- --text "brand name, brand shop:PHRASE, brand store" --match-type EXACT
678
- baker ads google keyword-lists add --customer-id 1234567890 --list-ref <list-id> --file negatives.txt
679
- # negatives.txt: one keyword[:MATCH_TYPE] per line; blank lines and "# comments" are skipped
680
- # Responsive search ad
681
- baker ads google ads create --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> \
682
- --headlines "Fast Widgets,Buy Online,Free Shipping" --descriptions "Best widgets around.,Ships tomorrow." \
683
- --final-url https://example.com
684
-
685
- # Review / undo staged changes
686
- baker ads google draft list # readable campaign ▸ ad group ▸ ad tree + completeness advisories
687
- baker ads google draft list --json # raw JSON envelope for scripting
688
- baker ads google draft remove g_temp_ab12 # cascades to dependents
689
- baker ads google draft clear
690
- ```
691
-
692
- Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets`, `audiences`, `conversions`, `bidding-strategies`, `labels`, `campaign-criteria`, and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa` take major units; `--target-roas` a ratio. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
693
-
694
- **Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with the simulated/live mode banner and non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
695
-
696
- **Batch keyword adds** — `keywords add`, `negative-keywords add`, and `keyword-lists add` take a whole batch in one command: comma-separate `--text` entries and/or pass `--file <list.txt>` (one keyword per line). A `:EXACT`/`:PHRASE`/`:BROAD` suffix per entry overrides the `--match-type` default. Batches stage all-or-nothing as one request (limit 500); each keyword still lands as its own draft op, so it stays individually removable/amendable.
697
-
698
- ---
699
-
700
656
  ### Caching
701
657
 
702
658
  Google Ads data is cached at two levels:
@@ -2574,12 +2530,25 @@ Auth: same `BAKER_API_KEY` + `BAKER_API_URL` as the rest of the CLI.
2574
2530
  ```bash
2575
2531
  # 1. Validate (free, never calls the API)
2576
2532
  baker canvas validate my-canvas.json
2533
+ # Blocking issues exit non-zero. Advisory `warnings[]` (exit 0) flag likely-wrong-but-
2534
+ # legal config to review before billing — a frame that describes a recurring element
2535
+ # (a pet/product) but doesn't wire its reference (VIDEO_REFERENCE_MISSING), or a scene
2536
+ # span that exceeds the assigned model's max clip duration (VIDEO_SPAN_EXCEEDS_MODEL).
2577
2537
 
2578
2538
  # 2. Run (executes nodes, writes files to ./canvas/<run_id>/)
2579
2539
  baker canvas run my-canvas.json
2580
2540
  # Only nodes reachable from the declared output execute (orphaned nodes are skipped,
2581
2541
  # not billed). Add --keep-runs N to prune old r_* run dirs, keeping the N newest.
2582
2542
 
2543
+ # 2b. Independent nodes ALREADY fan out in parallel — every node in the same
2544
+ # topological layer (e.g. all the per-clip video_generate nodes) runs concurrently
2545
+ # up to a bound. Raise it with --parallel N (alias: --concurrency N; default 5;
2546
+ # env BAKER_CANVAS_CONCURRENCY). A single clip failing NO LONGER strands its
2547
+ # siblings: the whole layer settles, cached siblings survive, and a re-run resumes
2548
+ # from cache. There is no need to render clips one at a time or pin `output` to a
2549
+ # single node — that old serial workaround is obsolete.
2550
+ baker canvas run my-canvas.json --parallel 8
2551
+
2583
2552
  # 3. Inspect a finished run (per-node timing, file list, optional video thumbs)
2584
2553
  baker canvas inspect <run_id>
2585
2554
 
@@ -3323,6 +3292,15 @@ Ref-image MIMEs: `image/png`, `image/jpeg`, `image/webp`, `image/gif` (via OpenR
3323
3292
  "duration": 6, "resolution": "1080p", "aspect_ratio": "16:9", "enhance_prompt": true } }
3324
3293
  ```
3325
3294
 
3295
+ > **Switching a clip's model has cross-node constraints** — validate after every model edit. `validate` gates all of these before any billed run:
3296
+ > - **One aspect_ratio for the whole canvas.** Every `video_generate` node must agree (`VIDEO_ASPECT_MISMATCH`), or the composite silently crops/letterboxes. Switching one clip to Veo (which only offers `16:9`/`9:16`) forces every other clip to a shared ratio too.
3297
+ > - **`duration` is a per-model hard enum.** Seedance `4,5,6,8,10,12,15`; Veo `4,6,8` only. A value outside the model's set is rejected; a scene whose natural span exceeds the model's max is flagged (`VIDEO_SPAN_EXCEEDS_MODEL`, advisory) — a 9.2s scene can't fit Veo's 8s cap and would truncate; split the scene or keep it on a longer-max model.
3298
+ > - **`person_generation` differs.** Veo accepts only `allow_all`.
3299
+ >
3300
+ > A scaffolded canvas carries this table inline at `metadata.todo.model_constraints`.
3301
+
3302
+ > **Content-policy blocks are deterministic, not flaky.** fal.ai/Seedance rejects any first/last frame that reads as a real-person likeness (even an AI-generated face) — surfaced as `content_policy_blocked` (HTTP 422, **non-retryable**), even when fal's proxy chain masks it as a 5xx. Retrying **never** succeeds and wastes credits. Fix the cause: switch the clip to the other curated model (Veo routes around fal's filter), or make the source frame less photorealistic.
3303
+
3326
3304
  ---
3327
3305
 
3328
3306
  ##### `tts`
@@ -24,9 +24,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  mod
25
25
  ));
26
26
 
27
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
27
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
28
28
  var require_safe_stable_stringify = __commonJS({
29
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
29
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
30
30
  "use strict";
31
31
  var { hasOwnProperty } = Object.prototype;
32
32
  var stringify = configure2();
@@ -624,6 +624,7 @@ ${originalIndentation}`;
624
624
  import path15 from "path";
625
625
 
626
626
  // src/engine/client/http.ts
627
+ var CONTENT_POLICY_CODE = "content_policy_blocked";
627
628
  var BackendHttpError = class extends Error {
628
629
  detail;
629
630
  constructor(detail) {
@@ -640,6 +641,8 @@ function describe(d) {
640
641
  return `invalid request: ${d.message}`;
641
642
  case "provider":
642
643
  return `provider error${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
644
+ case "content_policy":
645
+ return `content policy blocked${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
643
646
  case "timeout":
644
647
  return `provider timeout${d.provider ? ` (${d.provider})` : ""}: ${d.message}`;
645
648
  case "server":
@@ -736,6 +739,9 @@ async function parseErrorBody(res) {
736
739
  return classifyHttpError(res.status, errObj, errObj.message ?? `HTTP ${res.status}`);
737
740
  }
738
741
  function classifyHttpError(status, errObj, message) {
742
+ if (errObj.code === CONTENT_POLICY_CODE) {
743
+ return { kind: "content_policy", status, provider: errObj.provider, message };
744
+ }
739
745
  if (status === 401 || status === 403) {
740
746
  return { kind: "unauthorized", status, message };
741
747
  }
@@ -774,6 +780,24 @@ function isAsyncJob(res) {
774
780
  return typeof res.job_id === "string";
775
781
  }
776
782
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
783
+ function failedJobError(error) {
784
+ if (error.code === CONTENT_POLICY_CODE) {
785
+ return new BackendHttpError({
786
+ kind: "content_policy",
787
+ status: error.status ?? 422,
788
+ provider: error.provider,
789
+ message: error.message ?? "content policy blocked"
790
+ });
791
+ }
792
+ return new BackendHttpError({
793
+ kind: "provider",
794
+ status: error.status ?? 502,
795
+ provider: error.provider,
796
+ code: error.code ?? "provider_error",
797
+ message: error.message ?? "deconstruct failed",
798
+ retryable: error.retryable ?? false
799
+ });
800
+ }
777
801
  var JOB_POLL_INTERVAL_MS = 3e3;
778
802
  var JOB_POLL_MAX_MS = 20 * 60 * 1e3;
779
803
  var BackendClient = class {
@@ -797,16 +821,7 @@ var BackendClient = class {
797
821
  }
798
822
  const job = await this.http.getJson(path16, signal);
799
823
  if (job.status === "completed") return job.result;
800
- if (job.status === "failed") {
801
- throw new BackendHttpError({
802
- kind: "provider",
803
- status: job.error.status ?? 502,
804
- provider: job.error.provider,
805
- code: job.error.code ?? "provider_error",
806
- message: job.error.message ?? "deconstruct failed",
807
- retryable: job.error.retryable ?? false
808
- });
809
- }
824
+ if (job.status === "failed") throw failedJobError(job.error);
810
825
  if (Date.now() > deadline) {
811
826
  throw new BackendHttpError({ kind: "timeout", message: `job ${jobId} did not finish in time` });
812
827
  }
@@ -845,6 +860,9 @@ function requireCredentialsFromEnv(env = process.env) {
845
860
  }
846
861
 
847
862
  // src/engine/engine/errors.ts
863
+ function isBlocking(issue) {
864
+ return issue.severity !== "warning";
865
+ }
848
866
  var CanvasError = class extends Error {
849
867
  constructor(message) {
850
868
  super(message);
@@ -895,6 +913,8 @@ function describeCause(c) {
895
913
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
896
914
  case "network":
897
915
  return c.cause instanceof Error ? `network: ${c.cause.message}` : "network error";
916
+ case "content_policy":
917
+ return `CONTENT_POLICY_BLOCKED${c.provider ? ` (${c.provider})` : ""}: ${c.message}`;
898
918
  default: {
899
919
  const _exhaustive = c;
900
920
  return String(_exhaustive);
@@ -902,7 +922,7 @@ function describeCause(c) {
902
922
  }
903
923
  }
904
924
 
905
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
925
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
906
926
  var import__ = __toESM(require_safe_stable_stringify(), 1);
907
927
  var configure = import__.default.configure;
908
928
  var wrapper_default = import__.default;
@@ -1638,7 +1658,23 @@ var VideoMeta = z.object({
1638
1658
  // on which spoken beat" map emitted by scaffold-video (per-scene window,
1639
1659
  // spoken line, storyboard frames, scheduled graphics). Free-form rows so the
1640
1660
  // schema stays decoupled from the scaffold's exact shape.
1641
- motion_board: z.array(z.unknown()).optional()
1661
+ motion_board: z.array(z.unknown()).optional(),
1662
+ // The recurring identity elements the scaffold wired (one shared reference slot
1663
+ // per cast member / pet / product / logo). The reference-completeness check reads
1664
+ // this to warn when a frame's description mentions an element whose ref isn't
1665
+ // wired on that frame node (the wrong-identity-animal failure).
1666
+ elements: z.array(
1667
+ z.object({
1668
+ ref: z.string(),
1669
+ label: z.string(),
1670
+ type: z.string(),
1671
+ description: z.string().optional()
1672
+ })
1673
+ ).optional(),
1674
+ // Per video_generate node: the scene's natural visual span. The validator warns
1675
+ // when a clip's span exceeds the assigned model's max clip duration (e.g. a 9.2s
1676
+ // scene on Veo, which caps at 8s) — the clip would truncate.
1677
+ clip_spans: z.array(z.object({ node: z.string(), span_s: z.number() })).optional()
1642
1678
  }).strict().optional();
1643
1679
  var CanvasMetadata = z.object({
1644
1680
  name: z.string().optional(),
@@ -2136,8 +2172,11 @@ var STAGE_CODES = {
2136
2172
  AUDIO_DURATION: "VIDEO_AUDIO_DURATION",
2137
2173
  LIPSYNC_MISSING: "VIDEO_LIPSYNC_MISSING",
2138
2174
  SPEECH_OVERRUN: "VIDEO_SPEECH_OVERRUN",
2139
- ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH"
2175
+ ASPECT_MISMATCH: "VIDEO_ASPECT_MISMATCH",
2176
+ REFERENCE_MISSING: "VIDEO_REFERENCE_MISSING",
2177
+ SPAN_EXCEEDS_MODEL: "VIDEO_SPAN_EXCEEDS_MODEL"
2140
2178
  };
2179
+ var SPAN_MODEL_SLACK_S = 0.25;
2141
2180
  var VIDEO_TIME_SLACK_S = 0.75;
2142
2181
  var SPEECH_WORDS_PER_SECOND = 2.5;
2143
2182
  var SPEECH_OVERRUN_RATIO = 1.6;
@@ -2168,8 +2207,10 @@ function validateCanvas(input, registry) {
2168
2207
  const estimatedCredits = estimateCredits(ctx);
2169
2208
  checkOutputRef(ctx);
2170
2209
  checkVideoInvariants(ctx);
2171
- if (issues.length > 0) return { ok: false, issues };
2172
- return { ok: true, canvas, estimatedCredits };
2210
+ const hasBlocking = issues.some(isBlocking);
2211
+ if (hasBlocking) return { ok: false, issues };
2212
+ const warnings = issues.filter((i) => !isBlocking(i));
2213
+ return { ok: true, canvas, estimatedCredits, warnings: warnings.length > 0 ? warnings : void 0 };
2173
2214
  }
2174
2215
  async function validateCanvasDeep(input, registry) {
2175
2216
  const shallow = validateCanvas(input, registry);
@@ -2204,7 +2245,9 @@ async function validateCanvasDeep(input, registry) {
2204
2245
  });
2205
2246
  }
2206
2247
  }
2207
- if (issues.length > 0) return { ok: false, issues };
2248
+ const hasBlocking = issues.some(isBlocking);
2249
+ if (hasBlocking) return { ok: false, issues };
2250
+ const warnings = [...shallow.warnings ?? [], ...issues.filter((i) => !isBlocking(i))];
2208
2251
  const perNodeCredits = canvas.nodes.map((n) => {
2209
2252
  const def = registry.get(n.type);
2210
2253
  let credits = 0;
@@ -2217,7 +2260,13 @@ async function validateCanvasDeep(input, registry) {
2217
2260
  }
2218
2261
  return { node_id: n.id, node_type: n.type, credits };
2219
2262
  });
2220
- return { ok: true, canvas, estimatedCredits: shallow.estimatedCredits, perNodeCredits };
2263
+ return {
2264
+ ok: true,
2265
+ canvas,
2266
+ estimatedCredits: shallow.estimatedCredits,
2267
+ perNodeCredits,
2268
+ warnings: warnings.length > 0 ? warnings : void 0
2269
+ };
2221
2270
  }
2222
2271
  function buildIdToIndex(canvas) {
2223
2272
  const m = /* @__PURE__ */ new Map();
@@ -2504,6 +2553,95 @@ function checkVideoInvariants(ctx) {
2504
2553
  }
2505
2554
  checkSpeechOverrun(ctx, meta.talking_scenes);
2506
2555
  checkAspectConsistency(ctx);
2556
+ checkReferenceCompleteness(ctx, meta);
2557
+ checkClipSpanFitsModel(ctx, meta);
2558
+ }
2559
+ var ELEMENT_TYPE_KEYWORDS = {
2560
+ animal: ["dog", "puppy", "pup", "cat", "kitten", "kitty", "pet", "canine", "feline"],
2561
+ logo: ["logo", "wordmark"],
2562
+ badge: ["badge", "seal"],
2563
+ product: []
2564
+ };
2565
+ var CHECKED_ELEMENT_TYPES = new Set(Object.keys(ELEMENT_TYPE_KEYWORDS));
2566
+ var KEYWORD_STOPWORDS = /* @__PURE__ */ new Set([
2567
+ "the",
2568
+ "and",
2569
+ "with",
2570
+ "for",
2571
+ "brand",
2572
+ "logo",
2573
+ "image",
2574
+ "shot",
2575
+ "main",
2576
+ "hero",
2577
+ "element",
2578
+ "product",
2579
+ "reference"
2580
+ ]);
2581
+ function keywordTokens(text) {
2582
+ if (!text) return [];
2583
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !KEYWORD_STOPWORDS.has(t));
2584
+ }
2585
+ function keywordsForElement(el) {
2586
+ const type = el.type.toLowerCase();
2587
+ const typeWords = ELEMENT_TYPE_KEYWORDS[type] ?? [];
2588
+ return [.../* @__PURE__ */ new Set([...typeWords, ...keywordTokens(el.label), ...keywordTokens(el.description)])];
2589
+ }
2590
+ function containsWord(text, word) {
2591
+ const esc = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2592
+ return new RegExp(`\\b${esc}\\b`, "i").test(text);
2593
+ }
2594
+ function checkFrameReferences(ctx, node, index, keyworded) {
2595
+ const prompt = node.params?.prompt;
2596
+ if (typeof prompt !== "string" || prompt.length === 0) return;
2597
+ const inputsBlob = JSON.stringify(node.inputs ?? {});
2598
+ for (const { el, keywords } of keyworded) {
2599
+ if (inputsBlob.includes(el.ref)) continue;
2600
+ const hit = keywords.find((kw) => containsWord(prompt, kw));
2601
+ if (!hit) continue;
2602
+ ctx.issues.push({
2603
+ path: `nodes[${index}].inputs.reference`,
2604
+ code: STAGE_CODES.REFERENCE_MISSING,
2605
+ severity: "warning",
2606
+ message: `frame "${node.id}" describes a "${hit}" but the "${el.label}" reference (${el.ref}) isn't wired on this node \u2014 the generator will invent a wrong-identity ${el.type} from prose. Add ${el.ref} to inputs.reference`,
2607
+ node_id: node.id,
2608
+ node_type: node.type
2609
+ });
2610
+ }
2611
+ }
2612
+ function checkReferenceCompleteness(ctx, meta) {
2613
+ const elements = (meta.elements ?? []).filter((el) => CHECKED_ELEMENT_TYPES.has(el.type.toLowerCase()));
2614
+ const keyworded = elements.map((el) => ({ el, keywords: keywordsForElement(el) })).filter((e) => e.keywords.length > 0);
2615
+ if (keyworded.length === 0) return;
2616
+ for (let i = 0; i < ctx.canvas.nodes.length; i++) {
2617
+ const n = ctx.canvas.nodes[i];
2618
+ if (n?.type === "image_generate") checkFrameReferences(ctx, n, i, keyworded);
2619
+ }
2620
+ }
2621
+ function videoModelMaxDuration(model) {
2622
+ const spec = MODEL_REGISTRY.video_generate[model];
2623
+ const durations = spec?.params?.duration?.enum;
2624
+ if (!durations) return void 0;
2625
+ const nums = durations.filter((d) => typeof d === "number");
2626
+ return nums.length > 0 ? Math.max(...nums) : void 0;
2627
+ }
2628
+ function checkClipSpanFitsModel(ctx, meta) {
2629
+ for (const { node, span_s } of meta.clip_spans ?? []) {
2630
+ const target = ctx.canvas.nodes.find((n) => n.id === node && n.type === "video_generate");
2631
+ if (!target) continue;
2632
+ const model = target.params?.model;
2633
+ if (typeof model !== "string") continue;
2634
+ const max = videoModelMaxDuration(model);
2635
+ if (max === void 0 || span_s <= max + SPAN_MODEL_SLACK_S) continue;
2636
+ ctx.issues.push({
2637
+ path: `nodes[${ctx.idToIndex.get(node) ?? -1}].params.duration`,
2638
+ code: STAGE_CODES.SPAN_EXCEEDS_MODEL,
2639
+ severity: "warning",
2640
+ message: `scene for clip "${node}" needs ~${span_s}s but ${model} caps clips at ${max}s \u2014 the clip will truncate. Split the scene, or keep it on a model whose max duration covers the span`,
2641
+ node_id: node,
2642
+ node_type: "video_generate"
2643
+ });
2644
+ }
2507
2645
  }
2508
2646
  function speechOverrunOf(node) {
2509
2647
  const params = node.params;
@@ -2684,6 +2822,9 @@ var Engine = class {
2684
2822
  const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
2685
2823
  await writer.ensure();
2686
2824
  this.log(`[validate] ok (${canvas.nodes.length} nodes, est. ${validation.estimatedCredits} credits)`);
2825
+ for (const w of validation.warnings ?? []) {
2826
+ this.log(`[warn ] ${w.code}${w.node_id ? ` (${w.node_id})` : ""}: ${w.message}`);
2827
+ }
2687
2828
  const t0 = Date.now();
2688
2829
  const outputs = {};
2689
2830
  const counters = { cachedNodes: 0, totalCredits: 0 };
@@ -3253,6 +3394,13 @@ function mapClientError(ctx, e) {
3253
3394
  provider: d.provider
3254
3395
  });
3255
3396
  }
3397
+ if (d.kind === "content_policy") {
3398
+ return new NodeExecutionError(ctx.nodeId, ctx.nodeType, {
3399
+ kind: "content_policy",
3400
+ provider: d.provider,
3401
+ message: d.message
3402
+ });
3403
+ }
3256
3404
  if (d.kind === "timeout") {
3257
3405
  return new NodeExecutionError(ctx.nodeId, ctx.nodeType, { kind: "timeout", provider: d.provider });
3258
3406
  }
@@ -6351,4 +6499,4 @@ export {
6351
6499
  defaultRegistry,
6352
6500
  createEngineFromEnv
6353
6501
  };
6354
- //# sourceMappingURL=chunk-5AWO4BHJ.js.map
6502
+ //# sourceMappingURL=chunk-ZWMMCLJ4.js.map