@koda-sl/baker-cli 0.440.0-dev.dab0aaba9 → 0.444.0-dev.cac1750ec

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
@@ -198,7 +198,7 @@ import {
198
198
  withoutCastName,
199
199
  wordCount,
200
200
  ytDlpBlockSignal
201
- } from "./chunk-APTNJ2EY.js";
201
+ } from "./chunk-GY352NRN.js";
202
202
  import {
203
203
  NAME_CONTAINS_ARG,
204
204
  csvOrJson,
@@ -73631,11 +73631,18 @@ function autoFixStep(canvas, review, opts = {}) {
73631
73631
  pictureFindings.filter((f) => !BRIEF.test(withoutSize(f.what))).flatMap((f) => correctionOf(f.what) ?? [])
73632
73632
  );
73633
73633
  if (changes.length === 0) return sizesOnly();
73634
+ const flagged = new Set(blocking.flatMap((f) => sizesOf(f.what)));
73635
+ const keep = relaid.map((r) => r.format).filter((format) => !flagged.has(format));
73636
+ const remade = every.filter((size) => !keep.includes(size));
73637
+ const kept = {
73638
+ ...keep.length > 0 ? { keep } : {},
73639
+ ...remade.length > 0 ? { extend: remade } : {}
73640
+ };
73634
73641
  const asked = `Change only this: ${changes.join(" ")}`;
73635
73642
  const again = onPicture.filter((w) => persisting.has(w)).map((w) => w.replace(CATEGORY, "").replace(/\.$/, ""));
73636
73643
  const instruction = again.length > 0 ? `${asked} The last edit was already asked for this and Image 1 still shows it, so make it clearly: ${again.join("; ")}.` : asked;
73637
73644
  if (repeatsFailedFix(opts.lastFix, asked) || repeatsFailedFix(opts.lastFix, instruction)) return null;
73638
- return { kind: "fix", instruction, ...Object.keys(edits).length > 0 ? { relay: edits } : {}, ...afterNewPicture };
73645
+ return { kind: "fix", instruction, ...Object.keys(edits).length > 0 ? { relay: edits } : {}, ...kept };
73639
73646
  }
73640
73647
  var unique = (lines) => [...new Set(lines)];
73641
73648
  function deliveredSizes(canvas) {
@@ -73936,19 +73943,19 @@ async function variantImage(canvasRoot, runId, sha2562, fetchRecord = fetchRunRe
73936
73943
  }
73937
73944
 
73938
73945
  // src/commands/canvas/static-run-review.ts
73939
- import { readFile as readFile23 } from "fs/promises";
73946
+ import { readFile as readFile24 } from "fs/promises";
73940
73947
 
73941
73948
  // src/commands/canvas/glance-check.ts
73942
73949
  import sharp2 from "sharp";
73943
- import { z as z65 } from "zod";
73950
+ import { z as z66 } from "zod";
73944
73951
 
73945
73952
  // src/commands/canvas/viewer-check.ts
73946
73953
  import { createHash as createHash4 } from "crypto";
73947
- import { mkdir as mkdir8, readdir as readdir12, readFile as readFile22, stat as stat4, writeFile as writeFile13 } from "fs/promises";
73948
- import path28 from "path";
73954
+ import { mkdir as mkdir9, readdir as readdir12, readFile as readFile23, stat as stat4, writeFile as writeFile14 } from "fs/promises";
73955
+ import path29 from "path";
73949
73956
  import { defineCommand as defineCommand138 } from "citty";
73950
73957
  import { parse as parseYaml3 } from "yaml";
73951
- import { z as z64 } from "zod";
73958
+ import { z as z65 } from "zod";
73952
73959
 
73953
73960
  // src/commands/canvas/unbilled-spend.ts
73954
73961
  import { randomUUID as randomUUID2 } from "crypto";
@@ -74082,17 +74089,76 @@ function castingCheckFailure(answer, slug) {
74082
74089
  };
74083
74090
  }
74084
74091
 
74092
+ // src/commands/canvas/turn-renders.ts
74093
+ import { mkdir as mkdir8, readFile as readFile22, writeFile as writeFile13 } from "fs/promises";
74094
+ import path28 from "path";
74095
+ import { z as z64 } from "zod";
74096
+ var RUNS_PER_TURN = 2;
74097
+ var VIEWER_FAILS_PER_TURN = 2;
74098
+ var LedgerSchema = z64.object({
74099
+ turnId: z64.string(),
74100
+ creatives: z64.record(
74101
+ z64.string(),
74102
+ z64.object({ runs: z64.number().default(0), verdict: z64.string().optional(), viewerFails: z64.number().optional() })
74103
+ )
74104
+ });
74105
+ function turnRenderRefusal(ledger, turnId, slug) {
74106
+ const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
74107
+ if (!entry) return null;
74108
+ if (entry.verdict === "ready" || entry.verdict === "ready_with_notes") {
74109
+ return {
74110
+ code: "CONFLICT",
74111
+ message: `This ad is already ready in this turn (${entry.verdict}): its notes are not defects.`,
74112
+ fix: "Show it now. A change the person asks for comes with their next message."
74113
+ };
74114
+ }
74115
+ if (entry.runs >= RUNS_PER_TURN) {
74116
+ return {
74117
+ code: "CONFLICT",
74118
+ message: `This ad was rendered twice in this turn, its automatic corrections included, and is still not ready.`,
74119
+ fix: "Show the best version and say in one plain sentence what is left, in the person's words; they decide what changes next."
74120
+ };
74121
+ }
74122
+ return null;
74123
+ }
74124
+ function recordTurnRender(ledger, turnId, slug, verdict2) {
74125
+ const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
74126
+ return { turnId, creatives: { ...creatives, [slug]: { ...creatives[slug], runs: (creatives[slug]?.runs ?? 0) + 1, verdict: verdict2 } } };
74127
+ }
74128
+ function recordViewerFail(ledger, turnId, slug) {
74129
+ const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
74130
+ const entry = creatives[slug] ?? { runs: 0 };
74131
+ return { turnId, creatives: { ...creatives, [slug]: { ...entry, viewerFails: (entry.viewerFails ?? 0) + 1 } } };
74132
+ }
74133
+ function viewerChecksSpent(ledger, turnId, slug) {
74134
+ const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
74135
+ return (entry?.viewerFails ?? 0) >= VIEWER_FAILS_PER_TURN;
74136
+ }
74137
+ var ledgerPath = (cacheDir) => path28.join(cacheDir, "turn-renders.json");
74138
+ async function readTurnRenders(cacheDir) {
74139
+ try {
74140
+ const parsed = LedgerSchema.safeParse(JSON.parse(await readFile22(ledgerPath(cacheDir), "utf8")));
74141
+ return parsed.success ? parsed.data : void 0;
74142
+ } catch {
74143
+ return void 0;
74144
+ }
74145
+ }
74146
+ async function writeTurnRenders(cacheDir, ledger) {
74147
+ await mkdir8(cacheDir, { recursive: true });
74148
+ await writeFile13(ledgerPath(cacheDir), JSON.stringify(ledger), "utf8");
74149
+ }
74150
+
74085
74151
  // src/commands/canvas/viewer-check.ts
74086
74152
  var VIEWER_QUESTIONS = ["offer", "for_whom", "next_step"];
74087
74153
  var VIEWER_CLARITY = ["clear", "guess", "unclear"];
74088
- var ViewerReply = z64.object({ answer: z64.string().default(""), clarity: z64.enum(VIEWER_CLARITY) });
74089
- var ViewerAnswerSchema = z64.object({
74154
+ var ViewerReply = z65.object({ answer: z65.string().default(""), clarity: z65.enum(VIEWER_CLARITY) });
74155
+ var ViewerAnswerSchema = z65.object({
74090
74156
  offer: ViewerReply,
74091
74157
  for_whom: ViewerReply,
74092
74158
  next_step: ViewerReply,
74093
74159
  /** The words that talk to the viewer as if they had already done something with the brand; "" when none. */
74094
- presumes: z64.string().default(""),
74095
- missing: z64.string().default("")
74160
+ presumes: z65.string().default(""),
74161
+ missing: z65.string().default("")
74096
74162
  });
74097
74163
  var VIEWER_REQUIRED_CLARITY = {
74098
74164
  offer: "clear",
@@ -74199,14 +74265,14 @@ function viewerCheckCacheKey(input, model) {
74199
74265
  }
74200
74266
  async function cachedAnswer(file) {
74201
74267
  try {
74202
- const parsed = ViewerAnswerSchema.safeParse(JSON.parse(await readFile22(file, "utf8")));
74268
+ const parsed = ViewerAnswerSchema.safeParse(JSON.parse(await readFile23(file, "utf8")));
74203
74269
  return parsed.success ? parsed.data : null;
74204
74270
  } catch {
74205
74271
  return null;
74206
74272
  }
74207
74273
  }
74208
74274
  async function checkViewer(input, opts) {
74209
- const file = path28.join(opts.cacheDir, "viewer-check", `${viewerCheckCacheKey(input, opts.model)}.json`);
74275
+ const file = path29.join(opts.cacheDir, "viewer-check", `${viewerCheckCacheKey(input, opts.model)}.json`);
74210
74276
  let answer = await cachedAnswer(file);
74211
74277
  const cached = answer !== null;
74212
74278
  const prompt = buildViewerPrompt(input);
@@ -74216,8 +74282,8 @@ async function checkViewer(input, opts) {
74216
74282
  }
74217
74283
  if (answer === null) return { status: "unavailable", input };
74218
74284
  if (!cached) {
74219
- await mkdir8(path28.dirname(file), { recursive: true });
74220
- await writeFile13(file, `${JSON.stringify(answer, null, 2)}
74285
+ await mkdir9(path29.dirname(file), { recursive: true });
74286
+ await writeFile14(file, `${JSON.stringify(answer, null, 2)}
74221
74287
  `, "utf8");
74222
74288
  }
74223
74289
  const verdict2 = judgeViewerAnswer(answer, input.intended);
@@ -74248,7 +74314,10 @@ function viewerCheckFailure(outcome2, slug) {
74248
74314
  const who = describeViewer(viewer);
74249
74315
  const missing = outcome2.answer.missing.trim();
74250
74316
  const unsaid = outcome2.verdict.missed.flatMap((m) => m.question === "presumes" ? [] : [QUESTION_WORDS[m.question]]);
74317
+ const offer = outcome2.input.intended.offer?.trim();
74318
+ const rewrite = offer && outcome2.verdict.missed.some((m) => m.question === "offer") ? [`Do this: make the biggest words say "${offer}", plainly, in the ad's language; keep the rest.`] : [];
74251
74319
  const fix = [
74320
+ ...rewrite,
74252
74321
  ...unsaid.length > 0 ? [`Seen by ${who}, the words and picture alone do not say ${unsaid.join(" or ")}.`] : [`Seen by ${who}:`],
74253
74322
  ...outcome2.verdict.missed.map(missLine),
74254
74323
  ...missing ? [`They needed: ${missing}`] : [],
@@ -74267,17 +74336,17 @@ function viewerCheckFailure(outcome2, slug) {
74267
74336
  }
74268
74337
  };
74269
74338
  }
74270
- var RecordedViewer = z64.object({
74271
- temperature: z64.enum(VIEWER_TEMPERATURE_VALUES),
74272
- knows: z64.array(z64.enum(VIEWER_KNOWS_VALUES)).default([]),
74273
- placements: z64.array(z64.string().min(1)).default(["automatic"]),
74274
- source: z64.enum(VIEWER_SOURCE_VALUES),
74275
- reasons: z64.array(z64.string()).default([]),
74276
- market: z64.object({
74277
- country: z64.string().min(2),
74278
- language: z64.string().optional(),
74279
- currency: z64.string().optional(),
74280
- address: z64.string().optional()
74339
+ var RecordedViewer = z65.object({
74340
+ temperature: z65.enum(VIEWER_TEMPERATURE_VALUES),
74341
+ knows: z65.array(z65.enum(VIEWER_KNOWS_VALUES)).default([]),
74342
+ placements: z65.array(z65.string().min(1)).default(["automatic"]),
74343
+ source: z65.enum(VIEWER_SOURCE_VALUES),
74344
+ reasons: z65.array(z65.string()).default([]),
74345
+ market: z65.object({
74346
+ country: z65.string().min(2),
74347
+ language: z65.string().optional(),
74348
+ currency: z65.string().optional(),
74349
+ address: z65.string().optional()
74281
74350
  }).optional()
74282
74351
  });
74283
74352
  var nameKey = (text3) => wordsOf(text3).join(" ");
@@ -74336,12 +74405,12 @@ async function resolveViewerAlone(sources, lookups) {
74336
74405
  notes
74337
74406
  };
74338
74407
  }
74339
- var Frontmatter = z64.object({
74340
- viewer: z64.unknown().optional(),
74341
- targetAdSet: z64.union([z64.string(), z64.number()]).transform(String).optional(),
74342
- persona: z64.string().optional(),
74343
- awareness: z64.enum(AWARENESS_VALUES).optional().catch(void 0),
74344
- angle: z64.string().optional()
74408
+ var Frontmatter = z65.object({
74409
+ viewer: z65.unknown().optional(),
74410
+ targetAdSet: z65.union([z65.string(), z65.number()]).transform(String).optional(),
74411
+ persona: z65.string().optional(),
74412
+ awareness: z65.enum(AWARENESS_VALUES).optional().catch(void 0),
74413
+ angle: z65.string().optional()
74345
74414
  });
74346
74415
  function frontmatterOf2(raw) {
74347
74416
  const block2 = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
@@ -74354,8 +74423,8 @@ function frontmatterOf2(raw) {
74354
74423
  }
74355
74424
  }
74356
74425
  async function readCreativeForViewerCheck(creativeDir) {
74357
- const spec = readAdSpec(JSON.parse(await readFile22(path28.join(creativeDir, "prompt.json"), "utf8")));
74358
- const definition = frontmatterOf2(await readFile22(path28.join(creativeDir, "_definition.md"), "utf8").catch(() => ""));
74426
+ const spec = readAdSpec(JSON.parse(await readFile23(path29.join(creativeDir, "prompt.json"), "utf8")));
74427
+ const definition = frontmatterOf2(await readFile23(path29.join(creativeDir, "_definition.md"), "utf8").catch(() => ""));
74359
74428
  const offer = spec.message?.trim() || definition.angle?.trim();
74360
74429
  const persona = spec.viewer?.who.trim() || definition.persona?.trim();
74361
74430
  const brand = spec.brand?.name?.trim();
@@ -74384,7 +74453,7 @@ function castPeople(spec) {
74384
74453
  return people.length > 0 ? { people } : {};
74385
74454
  }
74386
74455
  async function readPersonas(workspaceRoot) {
74387
- const dir = path28.join(workspaceRoot, "src", "content", "personas");
74456
+ const dir = path29.join(workspaceRoot, "src", "content", "personas");
74388
74457
  let files;
74389
74458
  try {
74390
74459
  files = (await readdir12(dir, { recursive: true })).filter((f) => f.endsWith(".md"));
@@ -74393,7 +74462,7 @@ async function readPersonas(workspaceRoot) {
74393
74462
  }
74394
74463
  return await Promise.all(
74395
74464
  files.map(async (file) => {
74396
- const raw = await readFile22(path28.join(dir, file), "utf8");
74465
+ const raw = await readFile23(path29.join(dir, file), "utf8");
74397
74466
  const block2 = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
74398
74467
  let segment;
74399
74468
  try {
@@ -74403,7 +74472,7 @@ async function readPersonas(workspaceRoot) {
74403
74472
  }
74404
74473
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim();
74405
74474
  return {
74406
- slug: path28.basename(file, ".md"),
74475
+ slug: path29.basename(file, ".md"),
74407
74476
  ...title ? { title } : {},
74408
74477
  ...typeof segment === "string" && segment.trim() ? { segment: segment.trim() } : {}
74409
74478
  };
@@ -74442,8 +74511,8 @@ function liveViewerLookups(workspaceRoot) {
74442
74511
  return {
74443
74512
  adSetViewer: metaAdSetViewer,
74444
74513
  personas: () => readPersonas(workspaceRoot),
74445
- companyMarket: async () => marketFromCompanyDoc(await readFile22(path28.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => null)),
74446
- company: async () => (await readFile22(path28.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => "")).slice(0, 800)
74514
+ companyMarket: async () => marketFromCompanyDoc(await readFile23(path29.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => null)),
74515
+ company: async () => (await readFile23(path29.join(workspaceRoot, "COMPANY.md"), "utf8").catch(() => "")).slice(0, 800)
74447
74516
  };
74448
74517
  }
74449
74518
  function gatewayViewerModel(model, onSpend) {
@@ -74473,16 +74542,7 @@ async function viewerCheckBeforeRender(opts) {
74473
74542
  const who = describeViewer(resolved.viewer);
74474
74543
  const lines = resolved.notes.map((note) => `[viewer] ${note}`);
74475
74544
  if (opts.skip) return { viewer: resolved.viewer, lines: [...lines, `[viewer] not asked: ${SKIP_WORDS[opts.skip]}.`] };
74476
- const outcome2 = await checkViewer({ ...creative.ad, viewer: resolved.viewer }, opts);
74477
- if (outcome2.status === "unavailable") {
74478
- lines.push(
74479
- `[warn] viewer_check_unavailable: the viewer model did not answer, so this ad renders without the viewer check. Read the copy as ${who} before showing it, or run \`baker canvas viewer-check ${opts.slug}\` again.`
74480
- );
74481
- return { viewer: resolved.viewer, lines };
74482
- }
74483
- if (outcome2.status === "failed") return { viewer: resolved.viewer, lines, failure: viewerCheckFailure(outcome2, opts.slug) };
74484
- lines.push(`[viewer] ${who} understood it: ${outcome2.answer.offer.answer}${outcome2.cached ? " (cached)" : ""}.`);
74485
- const casting = await castingCheck(
74545
+ const casting = (async () => castingCheck(
74486
74546
  {
74487
74547
  people: creative.ad.people ?? [],
74488
74548
  company: await opts.lookups.company?.().catch(() => "") ?? "",
@@ -74490,10 +74550,26 @@ async function viewerCheckBeforeRender(opts) {
74490
74550
  ...creative.ad.intended.persona ? { persona: creative.ad.intended.persona } : {}
74491
74551
  },
74492
74552
  { cacheDir: opts.cacheDir, model: opts.model, ask: opts.ask }
74493
- );
74494
- if (casting.status === "failed") return { viewer: resolved.viewer, lines, failure: castingCheckFailure(casting.answer, opts.slug) };
74495
- if (casting.status === "passed") lines.push(`[casting] the people cast pass as real customers${casting.cached ? " (cached)" : ""}.`);
74496
- if (casting.status === "unavailable") {
74553
+ ))().catch(() => ({ status: "unavailable" }));
74554
+ const outcome2 = await checkViewer({ ...creative.ad, viewer: resolved.viewer }, opts);
74555
+ if (outcome2.status === "unavailable") {
74556
+ lines.push(
74557
+ `[warn] viewer_check_unavailable: the viewer model did not answer, so this ad renders without the viewer check. Read the copy as ${who} before showing it, or run \`baker canvas viewer-check ${opts.slug}\` again.`
74558
+ );
74559
+ } else if (outcome2.status === "failed" && !opts.spent) {
74560
+ return { viewer: resolved.viewer, lines, failure: viewerCheckFailure(outcome2, opts.slug) };
74561
+ } else if (outcome2.status === "failed") {
74562
+ lines.push(
74563
+ `[viewer] this ad failed the viewer check twice in this turn, so it renders as written. Tell the person what ${who} missed: ${outcome2.verdict.missed.map(missLine).join(" ")}`
74564
+ );
74565
+ } else {
74566
+ lines.push(`[viewer] ${who} understood it: ${outcome2.answer.offer.answer}${outcome2.cached ? " (cached)" : ""}.`);
74567
+ }
74568
+ if (outcome2.status === "unavailable") return { viewer: resolved.viewer, lines };
74569
+ const cast = await casting;
74570
+ if (cast.status === "failed") return { viewer: resolved.viewer, lines, failure: castingCheckFailure(cast.answer, opts.slug) };
74571
+ if (cast.status === "passed") lines.push(`[casting] the people cast pass as real customers${cast.cached ? " (cached)" : ""}.`);
74572
+ if (cast.status === "unavailable") {
74497
74573
  lines.push("[warn] casting_check_unavailable: the model did not answer; check yourself that the people cast look like this company's real customers.");
74498
74574
  }
74499
74575
  return { viewer: resolved.viewer, lines };
@@ -74514,11 +74590,11 @@ var ARGS5 = {
74514
74590
  };
74515
74591
  registerSchema({ command: "canvas.viewer-check", description: DESCRIPTION2, args: ARGS5 });
74516
74592
  async function creativeDirOf(arg, workspaceRoot) {
74517
- const direct = path28.resolve(arg);
74593
+ const direct = path29.resolve(arg);
74518
74594
  try {
74519
- return (await stat4(direct)).isDirectory() ? direct : path28.dirname(direct);
74595
+ return (await stat4(direct)).isDirectory() ? direct : path29.dirname(direct);
74520
74596
  } catch {
74521
- return path28.join(workspaceRoot, "src", "creatives", arg);
74597
+ return path29.join(workspaceRoot, "src", "creatives", arg);
74522
74598
  }
74523
74599
  }
74524
74600
  var viewerCheckCommand = defineCommand138({
@@ -74528,7 +74604,7 @@ var viewerCheckCommand = defineCommand138({
74528
74604
  const cwd = process.cwd();
74529
74605
  const workspaceRoot = findWorkspaceRoot(cwd) ?? cwd;
74530
74606
  const creativeDir = await creativeDirOf(String(args8.slug), workspaceRoot);
74531
- const slug = path28.basename(creativeDir);
74607
+ const slug = path29.basename(creativeDir);
74532
74608
  let creative;
74533
74609
  try {
74534
74610
  creative = await readCreativeForViewerCheck(creativeDir);
@@ -74545,7 +74621,7 @@ var viewerCheckCommand = defineCommand138({
74545
74621
  }
74546
74622
  const resolved = await resolveViewer(creative.sources, liveViewerLookups(workspaceRoot));
74547
74623
  const input = { ...creative.ad, viewer: resolved.viewer };
74548
- const cacheDir = path28.join(workspaceRoot, "canvas", ".cache");
74624
+ const cacheDir = path29.join(workspaceRoot, "canvas", ".cache");
74549
74625
  const spent = [];
74550
74626
  const outcome2 = await checkViewer(input, {
74551
74627
  cacheDir,
@@ -74562,6 +74638,19 @@ var viewerCheckCommand = defineCommand138({
74562
74638
  ...resolved.notes.length > 0 ? { notes: resolved.notes } : {}
74563
74639
  } : {};
74564
74640
  if (outcome2.status === "failed") {
74641
+ const turnId = optionalTurnId();
74642
+ const ledger = await readTurnRenders(cacheDir);
74643
+ if (viewerChecksSpent(ledger, turnId, slug)) {
74644
+ writeJson({
74645
+ ok: true,
74646
+ data: { checked: true, understood: false, viewer: who, missed: outcome2.verdict.missed, ...detail },
74647
+ hints: [
74648
+ `This ad failed the viewer check twice in this turn: render it as written (baker canvas run --slug ${slug}) and tell the person what ${who} missed: ${outcome2.verdict.missed.map(missLine).join(" ")}`
74649
+ ]
74650
+ });
74651
+ return;
74652
+ }
74653
+ if (turnId) await writeTurnRenders(cacheDir, recordViewerFail(ledger, turnId, slug));
74565
74654
  const failure = viewerCheckFailure(outcome2, slug);
74566
74655
  writeJson({ ...failure, error: { ...failure.error, ...detail } });
74567
74656
  process.exit(2);
@@ -74604,9 +74693,9 @@ var GLANCE_REQUIRED = {
74604
74693
  var GLANCE_WIDTH = 400;
74605
74694
  var GlanceAnswerSchema = ViewerAnswerSchema.extend({
74606
74695
  /** What kind of product or service they take it for (D4). */
74607
- category: z65.object({ answer: z65.string().default(""), clarity: z65.enum(["clear", "guess", "unclear"]).default("unclear") }).default({ answer: "", clarity: "unclear" }),
74608
- stopped: z65.string().default(""),
74609
- me: z65.enum(["yes", "no", "unsure"]).default("unsure")
74696
+ category: z66.object({ answer: z66.string().default(""), clarity: z66.enum(["clear", "guess", "unclear"]).default("unclear") }).default({ answer: "", clarity: "unclear" }),
74697
+ stopped: z66.string().default(""),
74698
+ me: z66.enum(["yes", "no", "unsure"]).default("unsure")
74610
74699
  });
74611
74700
  function wherePhrase2(placements) {
74612
74701
  const fullScreen = placements.length === 0 || placements.some((p) => /automatic|stor|reel/i.test(p));
@@ -74776,7 +74865,7 @@ function runCostHint(stats, kind) {
74776
74865
  async function textOf(ref2) {
74777
74866
  const r = ref2;
74778
74867
  try {
74779
- if (typeof r?.path === "string") return await readFile23(r.path, "utf8");
74868
+ if (typeof r?.path === "string") return await readFile24(r.path, "utf8");
74780
74869
  if (typeof r?.url === "string") {
74781
74870
  const res = await fetch(r.url);
74782
74871
  return res.ok ? await res.text() : null;
@@ -74843,7 +74932,7 @@ async function reviewStaticRun(opts) {
74843
74932
  let native;
74844
74933
  let palette = [];
74845
74934
  try {
74846
- const spec = readAdSpec(JSON.parse(await readFile23(targets.promptPath, "utf8")));
74935
+ const spec = readAdSpec(JSON.parse(await readFile24(targets.promptPath, "utf8")));
74847
74936
  copy = spec.copy.map((l) => l.text);
74848
74937
  lines = spec.copy.map((l) => ({ text: l.text, lettering: l.lettering, ...l.role ? { role: l.role } : {} }));
74849
74938
  if (spec.native) {
@@ -74877,8 +74966,10 @@ async function reviewStaticRun(opts) {
74877
74966
  () => null
74878
74967
  );
74879
74968
  const comparing = opts.previous ? nearIdenticalFindings(targets.images, opts.previous).catch(() => []) : Promise.resolve([]);
74969
+ const untouched = opts.settled ? await samePictureSizes(targets.images, opts.settled.images) : [];
74970
+ const changed = targets.images.filter((i) => !untouched.includes(i.format));
74880
74971
  const review = await reviewRenderedStatic({
74881
- images: targets.images,
74972
+ images: changed.length > 0 ? changed : targets.images,
74882
74973
  copy,
74883
74974
  lines,
74884
74975
  palette,
@@ -74901,7 +74992,7 @@ async function reviewStaticRun(opts) {
74901
74992
  const glance = review === null ? null : glanced;
74902
74993
  const unchanged = review === null ? [] : compared;
74903
74994
  const fresh = review === null ? null : [...review, ...glance ?? [], ...unchanged];
74904
- const reviewed = fresh === null || !opts.settled ? fresh : carryUnchanged(fresh, opts.settled.review, await samePictureSizes(targets.images, opts.settled.images));
74995
+ const reviewed = fresh === null || !opts.settled ? fresh : carryUnchanged(fresh, opts.settled.review, untouched);
74905
74996
  const verdict2 = staticVerdict(reviewed);
74906
74997
  const hints2 = verdictHints(verdict2);
74907
74998
  const sizeHints = verdict2 === "needs_fixes" ? sizeFixHints(opts.parsed, reviewed) : [];
@@ -75024,12 +75115,12 @@ function variantHints(judge, prefilter) {
75024
75115
  }
75025
75116
 
75026
75117
  // src/commands/canvas/static-spec-repair.ts
75027
- import { readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
75118
+ import { readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
75028
75119
  import sharp3 from "sharp";
75029
- import { z as z66 } from "zod";
75030
- var Answer = z66.object({
75031
- changed: z66.string().min(1),
75032
- set: z66.array(z66.object({ path: z66.string().min(1), value: z66.unknown() }))
75120
+ import { z as z67 } from "zod";
75121
+ var Answer = z67.object({
75122
+ changed: z67.string().min(1),
75123
+ set: z67.array(z67.object({ path: z67.string().min(1), value: z67.unknown() }))
75033
75124
  });
75034
75125
  function withValueAt(root, path68, value2) {
75035
75126
  const [key, ...rest] = path68;
@@ -75083,7 +75174,7 @@ function buildSpecRepairPrompt(spec, findings, schema) {
75083
75174
  var REPAIR_TIMEOUT_MS = 15e4;
75084
75175
  var SHOWN_WIDTH = 768;
75085
75176
  async function repairSpec(opts) {
75086
- const text3 = await readFile24(opts.promptPath, "utf8").catch(() => null);
75177
+ const text3 = await readFile25(opts.promptPath, "utf8").catch(() => null);
75087
75178
  if (text3 === null) return null;
75088
75179
  let before;
75089
75180
  try {
@@ -75095,57 +75186,11 @@ async function repairSpec(opts) {
75095
75186
  const prompt = buildSpecRepairPrompt(text3, opts.findings, adSpecJsonSchema());
75096
75187
  const repair = acceptRepair(before, await askAboutImages(RENDER_REVIEW_MODEL, shown ? [shown] : [], prompt, opts.onSpend, REPAIR_TIMEOUT_MS));
75097
75188
  if (!repair) return null;
75098
- await writeFile14(opts.promptPath, `${JSON.stringify(repair.spec, null, 2)}
75189
+ await writeFile15(opts.promptPath, `${JSON.stringify(repair.spec, null, 2)}
75099
75190
  `, "utf8");
75100
75191
  return repair.changed;
75101
75192
  }
75102
75193
 
75103
- // src/commands/canvas/turn-renders.ts
75104
- import { mkdir as mkdir9, readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
75105
- import path29 from "path";
75106
- import { z as z67 } from "zod";
75107
- var RUNS_PER_TURN = 2;
75108
- var LedgerSchema = z67.object({
75109
- turnId: z67.string(),
75110
- creatives: z67.record(z67.string(), z67.object({ runs: z67.number(), verdict: z67.string().optional() }))
75111
- });
75112
- function turnRenderRefusal(ledger, turnId, slug) {
75113
- const entry = turnId !== void 0 && ledger?.turnId === turnId ? ledger.creatives[slug] : void 0;
75114
- if (!entry) return null;
75115
- if (entry.verdict === "ready" || entry.verdict === "ready_with_notes") {
75116
- return {
75117
- code: "CONFLICT",
75118
- message: `This ad is already ready in this turn (${entry.verdict}): its notes are not defects.`,
75119
- fix: "Show it now. A change the person asks for comes with their next message."
75120
- };
75121
- }
75122
- if (entry.runs >= RUNS_PER_TURN) {
75123
- return {
75124
- code: "CONFLICT",
75125
- message: `This ad was rendered twice in this turn, its automatic corrections included, and is still not ready.`,
75126
- fix: "Show the best version and say in one plain sentence what is left, in the person's words; they decide what changes next."
75127
- };
75128
- }
75129
- return null;
75130
- }
75131
- function recordTurnRender(ledger, turnId, slug, verdict2) {
75132
- const creatives = ledger?.turnId === turnId ? ledger.creatives : {};
75133
- return { turnId, creatives: { ...creatives, [slug]: { runs: (creatives[slug]?.runs ?? 0) + 1, verdict: verdict2 } } };
75134
- }
75135
- var ledgerPath = (cacheDir) => path29.join(cacheDir, "turn-renders.json");
75136
- async function readTurnRenders(cacheDir) {
75137
- try {
75138
- const parsed = LedgerSchema.safeParse(JSON.parse(await readFile25(ledgerPath(cacheDir), "utf8")));
75139
- return parsed.success ? parsed.data : void 0;
75140
- } catch {
75141
- return void 0;
75142
- }
75143
- }
75144
- async function writeTurnRenders(cacheDir, ledger) {
75145
- await mkdir9(cacheDir, { recursive: true });
75146
- await writeFile15(ledgerPath(cacheDir), JSON.stringify(ledger), "utf8");
75147
- }
75148
-
75149
75194
  // src/commands/canvas/run.ts
75150
75195
  var OPERATOR = "Operator only, agents never need it.";
75151
75196
  var runCommand = defineCommand139({
@@ -75412,6 +75457,12 @@ ${describeRewrites(prepared.rewrites)}
75412
75457
  change: instruction.replace(/^Change only this: /, ""),
75413
75458
  ...opts.relayAlso ? { also: opts.relayAlso } : {}
75414
75459
  });
75460
+ const kept = delivered.filter((d) => opts.keepSizes?.includes(d.format));
75461
+ if (kept.length > 0) {
75462
+ parsed = withDeliveredSizes(parsed, Object.fromEntries(kept.map((d) => [d.format, d.path])));
75463
+ process.stdout.write(`[fix] keeping ${kept.map((d) => d.format).join(", ")} as it was: nothing blocking on it
75464
+ `);
75465
+ }
75415
75466
  if (!editInstruction) process.stdout.write(`[fix] correcting the version of run ${from}: ${instruction}
75416
75467
  `);
75417
75468
  }
@@ -75565,6 +75616,7 @@ ${describeRewrites(prepared.rewrites)}
75565
75616
  resumed: resumed && sameSource === true,
75566
75617
  asked: opts.skipViewerCheck === true
75567
75618
  }),
75619
+ spent: viewerChecksSpent(await readTurnRenders(turnCache), turnId, creativeSlug),
75568
75620
  lookups: liveViewerLookups(path30.dirname(canvasRoot)),
75569
75621
  model: VIEWER_CHECK_MODEL,
75570
75622
  ask: gatewayViewerModel(VIEWER_CHECK_MODEL, (usage) => viewerUsage.push(usage))
@@ -75573,6 +75625,9 @@ ${describeRewrites(prepared.rewrites)}
75573
75625
  `);
75574
75626
  await holdUnbilledSpend(cacheDir, viewerUsage);
75575
75627
  if (viewerCheck?.failure) {
75628
+ if (creativeSlug && turnId && viewerCheck.failure.error.code === "viewer_check_failed") {
75629
+ await writeTurnRenders(turnCache, recordViewerFail(await readTurnRenders(turnCache), turnId, creativeSlug));
75630
+ }
75576
75631
  process.stderr.write(`${JSON.stringify(viewerCheck.failure, null, 2)}
75577
75632
  `);
75578
75633
  process.exit(2);
@@ -75830,6 +75885,7 @@ ${describeRewrites(prepared.rewrites)}
75830
75885
  ...step.kind === "fix" ? {
75831
75886
  fix: step.instruction,
75832
75887
  ...step.relay ? { relayAlso: step.relay } : {},
75888
+ ...step.keep ? { keepSizes: step.keep } : {},
75833
75889
  ...step.extend ? { extendSizes: step.extend } : {}
75834
75890
  } : step.kind === "extend" ? { extendSizes: step.sizes, extendOnly: true } : step.kind === "relayout" ? {
75835
75891
  ...step.extend ? { extendSizes: step.extend } : {},
@@ -80998,7 +81054,7 @@ function ideasHints(report) {
80998
81054
  if (next.length === 1) hints2.push(`Build: \`baker creatives make ${next[0]}\`.`);
80999
81055
  if (next.length > 1)
81000
81056
  hints2.push(
81001
- `Build ${next.join(", ")} at once: one \`creative-producer\` per idea, all spawned in one message, each given its idea id. Never build them one after another: each render takes about five minutes.`
81057
+ `Build ${next.join(", ")} at once: one \`creative-producer\` per idea, all spawned in one message in the background, each given its idea id, and handle each as it returns. Never build them one after another: each render takes minutes.`
81002
81058
  );
81003
81059
  return hints2;
81004
81060
  }
@@ -81335,7 +81391,7 @@ function briefHints(parts) {
81335
81391
  }
81336
81392
  if (parts.customers.thin) {
81337
81393
  hints2.push(
81338
- `Only ${parts.customers.observed} customer quotes on file: run \`baker research customers\` before writing ideas, so they stand on what customers said.`
81394
+ `Only ${parts.customers.observed} customer quotes on file: run \`baker research customers\` now, before writing ideas, so they stand on what customers said. It gathers reviews, Reddit and the web and writes the quotes with their sources in about a minute; never a subagent or web searches of your own for this.`
81339
81395
  );
81340
81396
  }
81341
81397
  if (parts.bottleneck && isAvailable(parts.bottleneck) && parts.bottleneck.verdict === "after_click") {
@@ -81374,7 +81430,8 @@ function briefHints(parts) {
81374
81430
  return hints2;
81375
81431
  }
81376
81432
  async function readBrief(root, args8) {
81377
- const [company, offers, viewer, insights, ideas, performance, feedback, accountId] = await Promise.all([
81433
+ const account = metaAccountId(args8.accountId).catch(() => null);
81434
+ const [company, offers, viewer, insights, ideas, performance, feedback, accountId, bottleneck, market] = await Promise.all([
81378
81435
  companyStatus(root),
81379
81436
  readOffers(root),
81380
81437
  readViewer(root, args8.adset),
@@ -81387,10 +81444,8 @@ async function readBrief(root, args8) {
81387
81444
  )
81388
81445
  ),
81389
81446
  section(() => apiPost("/api/creatives/feedback", {})),
81390
- metaAccountId(args8.accountId).catch(() => null)
81391
- ]);
81392
- const [bottleneck, market] = await Promise.all([
81393
- accountId ? section(() => readBottleneck(accountId)) : null,
81447
+ account,
81448
+ account.then((id) => id ? section(() => readBottleneck(id)) : null),
81394
81449
  section(() => readMarket(root))
81395
81450
  ]);
81396
81451
  return { company, offers, viewer, insights, ideas, performance, feedback, accountId, bottleneck, market };
@@ -82172,8 +82227,9 @@ function makeEnvelope(made) {
82172
82227
  `Rewrite before running: ${made.unsourcedCopy.map((l) => JSON.stringify(l)).join(", ")} claim a figure nothing the company wrote holds (the reference's promise, not ours).`
82173
82228
  ] : [],
82174
82229
  `Write the copy lines and the scene in src/creatives/${made.slug}/prompt.json${made.reference ? ": the drafted lines are the reference's, so keep one only if the Knowledge says it (badges, ratings and guarantees too)" : ""}. Say the message in the customer's words${quote ? ` ("${quote.slice(0, 80)}")` : ""}; keep \`message\` as it is.`,
82175
- `Then: baker canvas run ${made.canvasPath}`,
82176
- ...made.scaffoldHints.slice(0, 4)
82230
+ `Then: baker canvas run ${made.canvasPath} (it validates and asks the viewer itself: nothing to run before it).`,
82231
+ // The scaffold's own next steps name a separate validate; the run does it (dev, Oct 2026: five validates before one run).
82232
+ ...made.scaffoldHints.filter((h) => !/canvas (?:validate|viewer-check)/.test(h)).slice(0, 4)
82177
82233
  ]
82178
82234
  };
82179
82235
  }