@koda-sl/baker-cli 0.194.0-dev.5dba041c4 → 0.194.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
@@ -47,7 +47,7 @@ import {
47
47
  toModelSafeImage,
48
48
  ulid,
49
49
  validateCanvasDeep
50
- } from "./chunk-5YX3QOT3.js";
50
+ } from "./chunk-O5GXFPWC.js";
51
51
  import {
52
52
  csvOrJson,
53
53
  daysAgoIso,
@@ -15740,6 +15740,7 @@ var PLACEMENT_TARGETING_DOC = "https://developers.facebook.com/docs/marketing-ap
15740
15740
  var CONSTRAINED_PLACEMENTS = [
15741
15741
  {
15742
15742
  position: "facebook_reels_overlay",
15743
+ reportedAs: "facebook_reels_overlay",
15743
15744
  platform: "facebook",
15744
15745
  kind: "constraint",
15745
15746
  note: "Ads on Facebook Reels take two shapes and a stock 9:16 video fits neither: overlay ads are 'a single, static image ad' (JPG/PNG, 1:1, 0% ratio tolerance, headline capped at 10 characters), and post-loop ads are 1:1 video of 4-15 seconds. Expect a long-headline 9:16 creative to look wrong here.",
@@ -15748,6 +15749,7 @@ var CONSTRAINED_PLACEMENTS = [
15748
15749
  },
15749
15750
  {
15750
15751
  position: "explore_home",
15752
+ reportedAs: "instagram_explore_grid_home",
15751
15753
  platform: "instagram",
15752
15754
  kind: "constraint",
15753
15755
  note: "Marketing API placement asset customization states 'The explore_home placement only supports the SINGLE_IMAGE format', but Meta's own Ads Guide lists Instagram Explore home under video specs. Check the account's play rate for this position before treating it as image-only.",
@@ -15756,6 +15758,7 @@ var CONSTRAINED_PLACEMENTS = [
15756
15758
  },
15757
15759
  {
15758
15760
  position: "right_hand_column",
15761
+ reportedAs: "right_hand_column",
15759
15762
  platform: "facebook",
15760
15763
  kind: "correction",
15761
15764
  note: "Facebook right column publishes full video specs (MP4/MOV/GIF, 1:1 at 1440x1440 or mobile-only 4:5 at 1440x1800, 1 second to 241 minutes) \u2014 it is NOT image-only. Its real constraint is that it cannot be the only placement for a video, collection, or canvas ad.",
@@ -15767,30 +15770,30 @@ var FACEBOOK_PREVIEW_FORMATS = {
15767
15770
  feed: "MOBILE_FEED_STANDARD",
15768
15771
  right_hand_column: "RIGHT_COLUMN_STANDARD",
15769
15772
  marketplace: "MARKETPLACE_MOBILE",
15770
- story: "FACEBOOK_STORY_MOBILE",
15773
+ facebook_stories: "FACEBOOK_STORY_MOBILE",
15771
15774
  facebook_reels: "FACEBOOK_REELS_MOBILE",
15772
15775
  facebook_reels_overlay: "FACEBOOK_REELS_BANNER",
15773
15776
  instream_video: "INSTREAM_VIDEO_MOBILE",
15774
15777
  video_feeds: "INSTREAM_VIDEO_MOBILE"
15775
15778
  };
15776
15779
  var INSTAGRAM_PREVIEW_FORMATS = {
15777
- stream: "INSTAGRAM_STANDARD",
15778
- story: "INSTAGRAM_STORY",
15779
- reels: "INSTAGRAM_REELS",
15780
- explore: "INSTAGRAM_EXPLORE_CONTEXTUAL",
15781
- explore_home: "INSTAGRAM_EXPLORE_GRID_HOME",
15782
- ig_search: "INSTAGRAM_SEARCH_CHAIN",
15783
- profile_feed: "INSTAGRAM_PROFILE_FEED"
15780
+ feed: "INSTAGRAM_STANDARD",
15781
+ instagram_stories: "INSTAGRAM_STORY",
15782
+ instagram_reels: "INSTAGRAM_REELS",
15783
+ instagram_explore: "INSTAGRAM_EXPLORE_CONTEXTUAL",
15784
+ instagram_explore_grid_home: "INSTAGRAM_EXPLORE_GRID_HOME",
15785
+ instagram_search: "INSTAGRAM_SEARCH_CHAIN",
15786
+ instagram_profile_feed: "INSTAGRAM_PROFILE_FEED"
15784
15787
  };
15785
- var PLATFORM_SCOPED_POSITIONS = /* @__PURE__ */ new Set(["story", "profile_feed"]);
15788
+ var PLATFORM_SCOPED_POSITIONS = /* @__PURE__ */ new Set(["feed"]);
15786
15789
  function previewFormatForPosition(position, platform) {
15787
15790
  if (platform === "facebook") return FACEBOOK_PREVIEW_FORMATS[position];
15788
15791
  if (platform === "instagram") return INSTAGRAM_PREVIEW_FORMATS[position];
15789
15792
  if (PLATFORM_SCOPED_POSITIONS.has(position)) return void 0;
15790
15793
  return FACEBOOK_PREVIEW_FORMATS[position] ?? INSTAGRAM_PREVIEW_FORMATS[position];
15791
15794
  }
15792
- function placementFormatNote(position) {
15793
- return CONSTRAINED_PLACEMENTS.find((p) => p.position === position);
15795
+ function placementFormatNote(reportedPosition) {
15796
+ return CONSTRAINED_PLACEMENTS.find((p) => p.reportedAs === reportedPosition);
15794
15797
  }
15795
15798
  function positionsMissingRequiredFeed(targeting) {
15796
15799
  const positions = targeting?.facebook_positions;
@@ -17525,8 +17528,8 @@ var metaDraftCommand = defineCommand62({
17525
17528
  import { defineCommand as defineCommand63 } from "citty";
17526
17529
 
17527
17530
  // src/commands/ads/meta/placement-delivery.ts
17528
- var MIN_IMPRESSIONS = 100;
17529
- var SILENT_PLAY_RATE = 0.02;
17531
+ var MIN_IMPRESSIONS = 20;
17532
+ var SILENT_FRACTION_OF_REFERENCE = 0.1;
17530
17533
  var PLAYING_REFERENCE_RATE = 0.5;
17531
17534
  function toNumber(value) {
17532
17535
  const n = Number(value);
@@ -17576,10 +17579,17 @@ function placementDeliveryHints(rows) {
17576
17579
  const carriesVideoMetrics = rows.some((row) => Array.isArray(row.video_play_actions));
17577
17580
  if (!carriesVideoMetrics || !isSingleAdQuery(rows)) return [];
17578
17581
  const placements = aggregate(rows);
17579
- const plays = placements.some((p) => p.impressions >= MIN_IMPRESSIONS && p.playRate >= PLAYING_REFERENCE_RATE);
17580
- if (!plays) return [];
17581
- const silent = placements.filter((p) => p.impressions >= MIN_IMPRESSIONS && p.playRate < SILENT_PLAY_RATE).sort((a, b) => b.impressions - a.impressions);
17582
- if (silent.length === 0) return [];
17582
+ const judgeable = placements.filter((p) => p.impressions >= MIN_IMPRESSIONS);
17583
+ const impressions = judgeable.reduce((total, p) => total + p.impressions, 0);
17584
+ const reference = impressions > 0 ? judgeable.reduce((total, p) => total + p.plays, 0) / impressions : 0;
17585
+ if (reference < PLAYING_REFERENCE_RATE) return [];
17586
+ const silent = judgeable.filter((p) => p.playRate < reference * SILENT_FRACTION_OF_REFERENCE).sort((a, b) => b.impressions - a.impressions);
17587
+ if (silent.length === 0) {
17588
+ const worst = [...judgeable].sort((a, b) => a.playRate - b.playRate)[0];
17589
+ return [
17590
+ `Checked every placement above ${MIN_IMPRESSIONS} impressions against this ad's own play rate (${(reference * 100).toFixed(0)}%): no placement is failing to play the video${worst ? `, the lowest being ${worst.platform}/${worst.position} at ${(worst.playRate * 100).toFixed(0)}%` : ""}. So placements do not explain a "it shows as a still image" report \u2014 rule out autoplay off or data saver in the viewer's own app, which renders every video as its cover frame account-wide.`
17591
+ ];
17592
+ }
17583
17593
  const hints = silent.map((placement) => {
17584
17594
  const note = placementFormatNote(placement.position);
17585
17595
  const documented = note ? ` Meta documents this placement: ${note.note}` : "";
@@ -17712,6 +17722,11 @@ function shouldAutoAsync(body) {
17712
17722
  }
17713
17723
  return false;
17714
17724
  }
17725
+ function wantsAsync(asyncFlag, body) {
17726
+ if (asyncFlag === true) return true;
17727
+ if (asyncFlag === false) return false;
17728
+ return shouldAutoAsync(body);
17729
+ }
17715
17730
  function applyDateRange(body, args) {
17716
17731
  const range = getDateRange(args);
17717
17732
  if (range.datePreset) body.datePreset = range.datePreset;
@@ -17760,9 +17775,9 @@ function buildBody(args) {
17760
17775
  async function pollAsync(reportRunId) {
17761
17776
  const start = Date.now();
17762
17777
  while (Date.now() - start < POLL_MAX_MS) {
17763
- const status = await apiPost(
17764
- `/api/ads/meta/insights/async/status?report-run-id=${encodeURIComponent(reportRunId)}`,
17765
- {}
17778
+ const status = await apiGet(
17779
+ "/api/ads/meta/insights/async/status",
17780
+ { "report-run-id": reportRunId }
17766
17781
  );
17767
17782
  if (status.async_status === "Job Completed") return;
17768
17783
  if (status.async_status === "Job Failed" || status.async_status === "Job Skipped") {
@@ -17860,8 +17875,10 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
17860
17875
  "use-account-attribution": { type: "boolean", description: "Use account-saved attribution setting" },
17861
17876
  "use-unified-attribution": { type: "boolean", description: "Use ad-set unified attribution setting" },
17862
17877
  limit: { type: "string", description: "Max rows (default 1000)" },
17863
- async: { type: "boolean", description: "Force async submit-and-poll path" },
17864
- "no-async": { type: "boolean", description: "Refuse the auto-async fallback for heavy queries" },
17878
+ // Declaring `no-async` too would be a trap: citty resolves it to `async: false`
17879
+ // and never fills an arg of that name, so the declaration reads as supported
17880
+ // while nothing consumes it.
17881
+ async: { type: "boolean", description: "Force the async submit-and-poll path (--no-async refuses it)" },
17865
17882
  "no-sort": { type: "boolean", description: "Skip default spend-desc sort" },
17866
17883
  "skip-cache": { type: "boolean", description: "Bypass server-side cache" },
17867
17884
  output: { type: "string", description: "Output format", default: "json" }
@@ -17880,7 +17897,7 @@ Async is automatic for heavy queries; pass --async to force it, or --no-async to
17880
17897
  }
17881
17898
  const body = buildBody(args);
17882
17899
  const fmt = csvOrJson(args);
17883
- const wantAsync = Boolean(args.async) || !args["no-async"] && shouldAutoAsync(body);
17900
+ const wantAsync = wantsAsync(args.async, body);
17884
17901
  try {
17885
17902
  const rows = wantAsync ? await runAsync(body) : await apiPost("/api/ads/meta/insights", body);
17886
17903
  const sorted = args["no-sort"] ? rows : sortRowsBySpendDesc(rows);
@@ -18093,11 +18110,11 @@ function resolvePlacement(position, platform) {
18093
18110
  }
18094
18111
  if (PLATFORM_SCOPED_POSITIONS.has(position) && !platformArg) {
18095
18112
  failPreviewValidation(
18096
- `--placement ${position} names a different surface on Facebook and on Instagram \u2014 add --platform facebook or --platform instagram`
18113
+ `--placement ${position} names a different surface on Facebook and on Instagram \u2014 add --platform facebook or --platform instagram (the breakdown row's publisher_platform)`
18097
18114
  );
18098
18115
  }
18099
18116
  failPreviewValidation(
18100
- `No Meta preview format is published for placement "${position}". Meta does not document one for every placement (Facebook notifications and profile feed, Instagram search among them), so this placement cannot be previewed \u2014 report it as unverified rather than inferring what it renders. Previewable placements: ${listPlacements()}`
18117
+ `No Meta preview format is published for placement "${position}". Meta does not document one for every placement (facebook_notification and facebook_profile_feed among them), so this placement cannot be previewed \u2014 report it as unverified rather than inferring what it renders. Pass the platform_position exactly as the insights breakdown spelled it: targeting names a few surfaces differently (story, reels, explore_home) and those names never appear in a breakdown. Previewable placements: ${listPlacements()}`
18101
18118
  );
18102
18119
  }
18103
18120
  function listPlacements() {
@@ -18132,7 +18149,7 @@ Examples:
18132
18149
  type: "string",
18133
18150
  description: "platform_position from an insights breakdown (e.g. facebook_reels_overlay) \u2014 resolved to an ad_format"
18134
18151
  },
18135
- platform: { type: "string", description: "facebook|instagram \u2014 disambiguates --placement story / profile_feed" },
18152
+ platform: { type: "string", description: "facebook|instagram \u2014 required for --placement feed, which both report" },
18136
18153
  "list-formats": { type: "boolean", description: "Print the ad_format values and placement mappings, then exit" },
18137
18154
  "out-file": { type: "string", description: "Save iframe HTML to this path" },
18138
18155
  "skip-cache": { type: "boolean", description: "Bypass server-side cache" }