@koda-sl/baker-cli 0.426.0-dev.5af7a82af → 0.427.0-dev.ad97d3ceb

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
@@ -80430,9 +80430,43 @@ function normalize(value2) {
80430
80430
  function sentencesOf(message) {
80431
80431
  return message.split(/[.!?]+(?:\s|$)/).filter((s) => s.trim().length > 0).length;
80432
80432
  }
80433
+ var FILLER = /* @__PURE__ */ new Set([
80434
+ "that",
80435
+ "this",
80436
+ "with",
80437
+ "your",
80438
+ "from",
80439
+ "they",
80440
+ "them",
80441
+ "what",
80442
+ "when",
80443
+ "have",
80444
+ "will",
80445
+ "into",
80446
+ "than",
80447
+ "more",
80448
+ "just",
80449
+ "para",
80450
+ "como",
80451
+ "pero",
80452
+ "sobre"
80453
+ ]);
80454
+ var SAME_MESSAGE = 0.6;
80455
+ function messageWords(message) {
80456
+ return new Set(
80457
+ message.toLowerCase().split(/[^\p{L}]+/u).filter((w) => w.length >= 4 && !FILLER.has(w)).map((w) => w.replace(/s$/, ""))
80458
+ );
80459
+ }
80460
+ function sameMessage(a, b) {
80461
+ const wa = messageWords(a);
80462
+ const wb = messageWords(b);
80463
+ const shared = [...wa].filter((w) => wb.has(w)).length;
80464
+ const all = (/* @__PURE__ */ new Set([...wa, ...wb])).size;
80465
+ return normalize(a) === normalize(b) || all > 0 && shared / all >= SAME_MESSAGE;
80466
+ }
80433
80467
  function repeatedFailure(idea, failed) {
80434
80468
  const lost = failed.find(
80435
- (f) => f.id !== idea.id && (normalize(f.message) === normalize(idea.message) || f.angle === idea.angle && f.format === idea.format)
80469
+ (f) => f.id !== idea.id && (sameMessage(f.message, idea.message) || f.angle === idea.angle && f.format === idea.format)
80436
80470
  );
80437
80471
  return lost ? [`says what "${lost.id}" said, and that idea failed in the account: change the message, not just the picture`] : [];
80438
80472
  }
@@ -80511,6 +80545,7 @@ function checkOne(idea, ctx) {
80511
80545
  return {
80512
80546
  id: idea.id,
80513
80547
  status: idea.status,
80548
+ message: idea.message,
80514
80549
  evidence,
80515
80550
  ...best ? { quote: best.quote } : {},
80516
80551
  awareness: idea.awareness,
@@ -81142,6 +81177,15 @@ function briefHints(parts) {
81142
81177
  `Competitors already say ${shared.join(", ")}: an idea that says the same needs its own proof or mechanism.`
81143
81178
  );
81144
81179
  }
81180
+ for (const won of parts.learned.winners) {
81181
+ hints2.push(
81182
+ `${won.idea} won in the account ("${won.message}"): build on it first, the same message in a new format or for a new persona.`
81183
+ );
81184
+ }
81185
+ if (parts.learned.failed.length > 0) {
81186
+ const lost = parts.learned.failed.map((f) => `"${f.message}"`).join(", ");
81187
+ hints2.push(`Never repeat a message that lost in the account: ${lost}.`);
81188
+ }
81145
81189
  if (parts.rejected > parts.approved && parts.rejected > 0) {
81146
81190
  hints2.push("Reviewers rejected more than they approved: read topRejectionReasons and avoid them in every idea.");
81147
81191
  }
@@ -81195,6 +81239,8 @@ function briefEnvelope(brief2, full) {
81195
81239
  const summary = isAvailable(brief2.feedback) ? brief2.feedback.data.summary : null;
81196
81240
  const next = buildNext(brief2.ideas.ideas);
81197
81241
  const toBuild = brief2.ideas.ideas.filter((i) => i.status === "idea").length;
81242
+ const withStatus = (status) => brief2.ideas.ideas.filter((i) => i.status === status).map((i) => ({ idea: i.id, message: i.message }));
81243
+ const learned = { winners: withStatus("winner"), failed: withStatus("failed") };
81198
81244
  return {
81199
81245
  ok: true,
81200
81246
  data: {
@@ -81209,7 +81255,13 @@ function briefEnvelope(brief2, full) {
81209
81255
  } : brief2.feedback,
81210
81256
  customers: full ? { ...customers, quotes: brief2.insights.map((i) => ({ id: i.id, type: i.type, quote: i.quote })) } : customers,
81211
81257
  market: marketPart(brief2.market, full),
81212
- ideas: { to_build: toBuild, built: brief2.ideas.ideas.length - toBuild, build_next: next }
81258
+ ideas: {
81259
+ to_build: toBuild,
81260
+ built: brief2.ideas.ideas.length - toBuild,
81261
+ build_next: next,
81262
+ ...learned.winners.length > 0 ? { winners: learned.winners } : {},
81263
+ ...learned.failed.length > 0 ? { failed: learned.failed } : {}
81264
+ }
81213
81265
  },
81214
81266
  hints: briefHints({
81215
81267
  company: brief2.company,
@@ -81219,7 +81271,8 @@ function briefEnvelope(brief2, full) {
81219
81271
  buildNext: next,
81220
81272
  approved: summary?.approved ?? 0,
81221
81273
  rejected: summary?.rejected ?? 0,
81222
- market: isAvailable(brief2.market) ? brief2.market : null
81274
+ market: isAvailable(brief2.market) ? brief2.market : null,
81275
+ learned
81223
81276
  })
81224
81277
  };
81225
81278
  }
@@ -81387,6 +81440,7 @@ var WINNER_RATIO = 0.8;
81387
81440
  var WINNER_MIN_RESULTS = 3;
81388
81441
  var FAILED_RATIO = 1.5;
81389
81442
  var SILENT_SPEND = 2;
81443
+ var SETTLED = /* @__PURE__ */ new Set(["winner", "failed", "inconclusive"]);
81390
81444
  var round4 = (n) => Math.round(n * 100) / 100;
81391
81445
  function totals(creatives) {
81392
81446
  const spend = creatives.reduce((sum, c) => sum + c.spend, 0);
@@ -81405,33 +81459,38 @@ function verdict(result, usual) {
81405
81459
  }
81406
81460
  return result.impressions >= SEEN_ENOUGH ? "inconclusive" : "testing";
81407
81461
  }
81462
+ function ideaResult(own, usual, asOf) {
81463
+ const mine = totals(own);
81464
+ const costPerResult = mine.results > 0 ? round4(mine.spend / mine.results) : null;
81465
+ return {
81466
+ asOf,
81467
+ creatives: own.length,
81468
+ ...own[0]?.currency ? { currency: own[0].currency } : {},
81469
+ spend: round4(mine.spend),
81470
+ impressions: mine.impressions,
81471
+ results: mine.results,
81472
+ costPerResult,
81473
+ ctr: mine.ctr,
81474
+ vsAccount: usual !== null && costPerResult !== null ? round4(costPerResult / usual) : null
81475
+ };
81476
+ }
81408
81477
  function learnFromResults(ideas, creatives, asOf) {
81409
81478
  const ideaOf = (c) => c.definition?.idea ?? null;
81410
81479
  return ideas.flatMap((idea) => {
81411
81480
  if (idea.status === "idea" || idea.status === "retired") return [];
81412
81481
  const own = creatives.filter((c) => ideaOf(c) === idea.id);
81413
81482
  if (own.length === 0) return [];
81414
- const mine = totals(own);
81415
81483
  const rest = totals(creatives.filter((c) => ideaOf(c) !== idea.id));
81416
81484
  const usual = rest.results > 0 ? rest.spend / rest.results : null;
81417
- const costPerResult = mine.results > 0 ? round4(mine.spend / mine.results) : null;
81418
- const result = {
81419
- asOf,
81420
- creatives: own.length,
81421
- ...own[0]?.currency ? { currency: own[0].currency } : {},
81422
- spend: round4(mine.spend),
81423
- impressions: mine.impressions,
81424
- results: mine.results,
81425
- costPerResult,
81426
- ctr: mine.ctr,
81427
- vsAccount: usual !== null && costPerResult !== null ? round4(costPerResult / usual) : null
81428
- };
81429
- return [{ idea: idea.id, from: idea.status, to: verdict(result, usual), result }];
81485
+ const result = ideaResult(own, usual, asOf);
81486
+ const to = verdict(result, usual);
81487
+ if (to === "testing" && SETTLED.has(idea.status)) return [];
81488
+ return [{ idea: idea.id, from: idea.status, to, result }];
81430
81489
  });
81431
81490
  }
81432
81491
 
81433
81492
  // src/commands/creatives/make-model.ts
81434
- import { parseDocument as parseDocument3 } from "yaml";
81493
+ import { parse as parse2, parseDocument as parseDocument3, Scalar, visit } from "yaml";
81435
81494
  var REFERENCE_CANDIDATES = 6;
81436
81495
  function isVideoIdea(idea) {
81437
81496
  return IDEA_VIDEO_FORMATS.includes(idea.format);
@@ -81478,13 +81537,29 @@ function definitionFieldsOf(idea, evidence) {
81478
81537
  };
81479
81538
  }
81480
81539
  var FRONTMATTER3 = /^---\r?\n([\s\S]*?\r?\n)?---/;
81540
+ function readsAsText11(text3) {
81541
+ try {
81542
+ return parse2(text3, { version: "1.1" }) === text3;
81543
+ } catch {
81544
+ return false;
81545
+ }
81546
+ }
81481
81547
  function setDefinitionFields(raw, fields) {
81482
81548
  const match = raw.match(FRONTMATTER3);
81483
81549
  if (!match) return raw;
81484
81550
  const doc = parseDocument3(match[1] ?? "");
81485
81551
  if (doc.errors.length > 0) return raw;
81486
81552
  for (const [key, value2] of Object.entries(fields)) {
81487
- if (value2 !== void 0) doc.set(key, value2);
81553
+ if (value2 === void 0) continue;
81554
+ const node = doc.createNode(value2);
81555
+ visit(node, {
81556
+ Scalar(_, scalar) {
81557
+ if (typeof scalar.value === "string" && !readsAsText11(scalar.value)) {
81558
+ scalar.type = Scalar.QUOTE_DOUBLE;
81559
+ }
81560
+ }
81561
+ });
81562
+ doc.set(key, node);
81488
81563
  }
81489
81564
  return `---
81490
81565
  ${doc.toString()}---${raw.slice(match[0].length)}`;
@@ -81538,10 +81613,15 @@ var ARGS11 = {
81538
81613
  "dry-run": { type: "boolean", required: false, default: false, description: "Show the verdicts; write nothing" }
81539
81614
  };
81540
81615
  registerSchema({ command: "creatives.learn", description: DESCRIPTION9, args: ARGS11 });
81541
- function learnEnvelope(learnings, written) {
81616
+ function learnEnvelope(learnings, written, unreadable = []) {
81542
81617
  const winners = learnings.filter((l) => l.to === "winner").map((l) => l.idea);
81543
81618
  const failed = learnings.filter((l) => l.to === "failed").map((l) => l.idea);
81544
81619
  const hints2 = [];
81620
+ if (unreadable.length > 0) {
81621
+ hints2.push(
81622
+ `Could not read ${unreadable.join(", ")}, so their results were not written: fix what \`baker creatives ideas\` names, then run this again.`
81623
+ );
81624
+ }
81545
81625
  if (learnings.length === 0) {
81546
81626
  hints2.push(
81547
81627
  "No built idea has ads delivering yet: nothing to learn. Ideas count once a published ad shows their creative."
@@ -81591,17 +81671,27 @@ var learnCommand = defineCommand158({
81591
81671
  meta: { name: "learn", description: DESCRIPTION9 },
81592
81672
  args: ARGS11,
81593
81673
  async run({ args: args8 }) {
81594
- const root = findWorkspaceRoot(process.cwd()) ?? process.cwd();
81595
- const days = args8.days ? Number(args8.days) : void 0;
81596
- const performance = withDefinitions(
81597
- await apiPost("/api/creatives/performance", days ? { days } : {}),
81598
- (slug) => readDefinition2(root, slug)
81599
- );
81600
- const { ideas } = await readIdeas(root);
81601
- const learnings = learnFromResults(ideas, performance.data.creatives, today());
81602
- const dryRun = args8["dry-run"] === true;
81603
- if (!dryRun) await writeBack(root, learnings);
81604
- writeJson(learnEnvelope(learnings, !dryRun && learnings.length > 0));
81674
+ try {
81675
+ const root = findWorkspaceRoot(process.cwd()) ?? process.cwd();
81676
+ const days = parseDays(args8.days);
81677
+ const performance = withDefinitions(
81678
+ await apiPost("/api/creatives/performance", days ? { days } : {}),
81679
+ (slug) => readDefinition2(root, slug)
81680
+ );
81681
+ const { ideas, unreadable } = await readIdeas(root);
81682
+ const learnings = learnFromResults(ideas, performance.data.creatives, today());
81683
+ const dryRun = args8["dry-run"] === true;
81684
+ if (!dryRun) await writeBack(root, learnings);
81685
+ writeJson(
81686
+ learnEnvelope(
81687
+ learnings,
81688
+ !dryRun && learnings.length > 0,
81689
+ unreadable.map((u) => u.id)
81690
+ )
81691
+ );
81692
+ } catch (err) {
81693
+ failApi(err);
81694
+ }
81605
81695
  }
81606
81696
  });
81607
81697
 
@@ -83192,7 +83282,7 @@ function suffixLookup(hostname, options, out) {
83192
83282
 
83193
83283
  // ../../.pnpm-store/v10/links/@/tldts/7.4.15/b098b1960b5a1104e1fa4520104b6822dcb63a4c9724102fa5412b7e81c108e1/node_modules/tldts/dist/es6/index.js
83194
83284
  var RESULT = getEmptyResult();
83195
- function parse2(url, options) {
83285
+ function parse3(url, options) {
83196
83286
  return parseImpl(url, 5, suffixLookup, options, getEmptyResult());
83197
83287
  }
83198
83288
  function getPublicSuffix(url, options) {
@@ -83217,7 +83307,7 @@ function validateHostPattern(raw) {
83217
83307
  if (host === "" || host.includes("*")) {
83218
83308
  return { ok: false, reason: "Use a website address like api.example.com or *.example.com." };
83219
83309
  }
83220
- const parsed = parse2(host);
83310
+ const parsed = parse3(host);
83221
83311
  if (parsed.isIp || host.startsWith("[")) {
83222
83312
  return { ok: false, reason: "Use a website address, not an IP address." };
83223
83313
  }