@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-
|
|
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 } : {}, ...
|
|
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
|
|
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
|
|
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
|
|
73948
|
-
import
|
|
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
|
|
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 =
|
|
74089
|
-
var ViewerAnswerSchema =
|
|
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:
|
|
74095
|
-
missing:
|
|
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
|
|
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 =
|
|
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
|
|
74220
|
-
await
|
|
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 =
|
|
74271
|
-
temperature:
|
|
74272
|
-
knows:
|
|
74273
|
-
placements:
|
|
74274
|
-
source:
|
|
74275
|
-
reasons:
|
|
74276
|
-
market:
|
|
74277
|
-
country:
|
|
74278
|
-
language:
|
|
74279
|
-
currency:
|
|
74280
|
-
address:
|
|
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 =
|
|
74340
|
-
viewer:
|
|
74341
|
-
targetAdSet:
|
|
74342
|
-
persona:
|
|
74343
|
-
awareness:
|
|
74344
|
-
angle:
|
|
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
|
|
74358
|
-
const definition = frontmatterOf2(await
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
74446
|
-
company: async () => (await
|
|
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
|
|
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
|
-
|
|
74495
|
-
if (
|
|
74496
|
-
|
|
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 =
|
|
74593
|
+
const direct = path29.resolve(arg);
|
|
74518
74594
|
try {
|
|
74519
|
-
return (await stat4(direct)).isDirectory() ? direct :
|
|
74595
|
+
return (await stat4(direct)).isDirectory() ? direct : path29.dirname(direct);
|
|
74520
74596
|
} catch {
|
|
74521
|
-
return
|
|
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 =
|
|
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 =
|
|
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:
|
|
74608
|
-
stopped:
|
|
74609
|
-
me:
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
75118
|
+
import { readFile as readFile25, writeFile as writeFile15 } from "fs/promises";
|
|
75028
75119
|
import sharp3 from "sharp";
|
|
75029
|
-
import { z as
|
|
75030
|
-
var Answer =
|
|
75031
|
-
changed:
|
|
75032
|
-
set:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|