@koda-sl/baker-cli 0.444.0-dev.cac1750ec → 0.447.0-dev.397af7869

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1817,7 +1817,7 @@ baker creatives learn [--days <n>] [--dry-run] # what each idea'
1817
1817
  - `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
1818
1818
  - `ideas` is free and local. An idea's `evidence` resolves to `src/content/insights/` entries, or it is an `explore` bet. Numbers must be in the Knowledge or our own customers' quotes (a Reddit, forum or web quote never sources a claim), no two ideas may share an angle and format, and a review or testimonial format needs a quote from our own customer.
1819
1819
  - `learn` pools each built idea's creatives, judges them against the rest of the account's cost per result and writes `status` (`winner`, `failed`, `inconclusive`, `testing`) and a frozen `result` onto the idea. `ideas` then warns when a new idea repeats a failed one.
1820
- - `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1820
+ - `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. For a new creative it starts scaffolding the best-ranked one while the fits are asked (kept when it fits, removed whole when another is kept), so the reference's describe pass no longer waits for them. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1821
1821
 
1822
1822
  ### Competitive Intelligence (`baker research`)
1823
1823
 
@@ -5833,7 +5833,7 @@ cart, a trial, a donation, a registration); who it is for is reported but never
5833
5833
  `_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
5834
5834
  the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
5835
5835
  `viewer_check_failed` and a `fix` that starts with the one rewrite answering a missed offer (`Do this: make the biggest words say "<message>"`) and names what the viewer missed. After two failures of one ad in a chat turn the check no longer holds it back: the command answers `ok: true` with `understood: false` and what was missed, and `baker canvas run` renders it, printing the miss. The casting check is asked alongside the viewer. Answers are cached per copy, scene and viewer.
5836
- `baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
5836
+ Before either, a static ad whose scene quotes words, or names a price or a share, that the copy (or `scene_text`) does not hold is refused (`scene_words_not_in_copy`): the picture would print them and a correction round would erase them. An unreadable `prompt.json` names each wrong field in a line and shows that field's shape. `baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
5837
5837
  `--skip-viewer-check` renders without it; a model outage lets the render through with a warning). `--full` adds
5838
5838
  the viewer's whole answer and how the viewer was chosen.
5839
5839
 
@@ -5879,7 +5879,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
5879
5879
 
5880
5880
  **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5881
5881
 
5882
- **Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
5882
+ **Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`); sizes that do not show the same ad (`SIZES:`) are always the re-laid ones' fault, since every size follows the chosen picture, so they are laid out again to show exactly what it shows, never the picture edited to match them. A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
5883
5883
 
5884
5884
  **Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, and every size is made again from the corrected picture. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
5885
5885
 
@@ -3594,23 +3594,6 @@ function resolveConcurrency(...candidates) {
3594
3594
  }
3595
3595
  return DEFAULT_CONCURRENCY;
3596
3596
  }
3597
- async function mapWithConcurrency(items, limit, fn) {
3598
- const results = new Array(items.length);
3599
- let next = 0;
3600
- const worker = async () => {
3601
- while (next < items.length) {
3602
- const index = next++;
3603
- try {
3604
- results[index] = { status: "fulfilled", value: await fn(items[index], index) };
3605
- } catch (reason) {
3606
- results[index] = { status: "rejected", reason };
3607
- }
3608
- }
3609
- };
3610
- const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
3611
- await Promise.all(workers);
3612
- return results;
3613
- }
3614
3597
 
3615
3598
  // src/engine/lib/json-walk.ts
3616
3599
  function isContainerObject(value) {
@@ -6931,6 +6914,49 @@ function unwrittenProblems(spec) {
6931
6914
  }
6932
6915
  return problems;
6933
6916
  }
6917
+ var QUOTED = /"([^"]{2,})"|“([^”]{2,})”|(?<![\p{L}\p{N}])['‘]([^'’]{2,}?)['’](?![\p{L}\p{N}])/gu;
6918
+ var FIGURE = /[£$€]\s?\d[\d.,]*|\d+(?:[.,]\d+)?\s?%/g;
6919
+ var comparable = (text2) => text2.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}£$€%]+/gu, " ").trim();
6920
+ function sceneWordProblems(spec) {
6921
+ const held = [...spec.copy.map((l) => l.text), ...spec.scene_text].map(comparable);
6922
+ const holds = (text2) => {
6923
+ const want = comparable(text2);
6924
+ return want.length === 0 || held.some((h) => h.includes(want));
6925
+ };
6926
+ const scene = [spec.scene.description, spec.scene.layout ?? ""].join(" ");
6927
+ const quoted = [...scene.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter((q) => !holds(q));
6928
+ const figures = (scene.match(FIGURE) ?? []).map((f) => f.trim()).filter((f) => !holds(f));
6929
+ const unheld = [.../* @__PURE__ */ new Set([...quoted, ...figures])];
6930
+ if (unheld.length === 0) return [];
6931
+ return [
6932
+ `The scene asks the picture to show ${unheld.map((w) => `"${w}"`).join(", ")}, which the copy does not hold: the picture would print it and the run would then spend a round erasing it. Put each one the ad needs in \`copy\` as an \`in_scene\` line with \`on\` (the bill, the phone), or describe that surface without words (blank, blurred, face-down).`
6933
+ ];
6934
+ }
6935
+ var FIELD_SHAPES = {
6936
+ copy: '"copy": [{"text": "Book your free survey", "lettering": "designed", "role": "cta"}]',
6937
+ scene: '"scene": {"description": "a family on a sunny roof terrace", "look": "photo"}',
6938
+ colors: '"colors": [{"object": "the button", "name": "deep green", "hex": "#1F5C3A"}]',
6939
+ references: '"references": [{"id": "PRODUCT_1", "role": "product", "what": "the solar panel kit"}]',
6940
+ third_party_brands: '"third_party_brands": [{"name": "Trustpilot", "role": "proof", "logo": "BADGE_1"}]',
6941
+ mechanism: '"mechanism": [{"device": "a before and after", "why_it_works": "the change is seen, not told"}]',
6942
+ viewer: '"viewer": {"who": "homeowners with high energy bills", "knows": "nothing about us yet"}'
6943
+ };
6944
+ var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
6945
+ function adSpecErrorText(e) {
6946
+ if (!(e instanceof z7.ZodError)) return e instanceof Error ? e.message : String(e);
6947
+ const items = /* @__PURE__ */ new Map();
6948
+ for (const issue of e.issues) {
6949
+ const [field2, index, key] = issue.path;
6950
+ const at2 = typeof index === "number" ? `${String(field2)}[${index}]` : issue.path.map(String).join(".") || "the spec";
6951
+ const what = issue.code === "unrecognized_keys" ? `no field ${issue.keys.map((k) => `"${k}"`).join(", ")}` : key !== void 0 && typeof index === "number" ? `"${String(key)}" ${issue.message.replace(/^Invalid input: /, "")}` : issue.message.replace(/^Invalid input: /, "");
6952
+ items.set(at2, [...items.get(at2) ?? [], what]);
6953
+ }
6954
+ const fields = [...new Set(e.issues.map((i) => String(i.path[0] ?? "")))].filter((f) => FIELD_SHAPES[f]);
6955
+ return [
6956
+ ...[...items].map(([at2, whats]) => `${at2}: ${[...new Set(whats)].join("; ")}`),
6957
+ ...fields.map((f) => `Shape: ${FIELD_SHAPES[f]}`)
6958
+ ].join("\n");
6959
+ }
6934
6960
  function readAdSpec(raw) {
6935
6961
  if (isAdSpec(raw)) return AdSpecSchema.parse(raw);
6936
6962
  const advertiser = raw?.reference?.advertiser;
@@ -10209,6 +10235,17 @@ function unwrap(schema) {
10209
10235
  }
10210
10236
 
10211
10237
  // src/engine/engine/executor.ts
10238
+ function throwRunOutcome(signal, failures, held) {
10239
+ if (signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10240
+ if (failures.length === 1 && failures[0]) throw failures[0].reason;
10241
+ if (failures.length > 1) throw new LayerExecutionError([...failures]);
10242
+ if (held.length > 0) {
10243
+ throw new RunAbortedError(
10244
+ "checkpoint",
10245
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10246
+ );
10247
+ }
10248
+ }
10212
10249
  function heldByCheckpoint(canvas, layer, kinds) {
10213
10250
  if (!kinds?.size) return [];
10214
10251
  return layer.filter((id) => {
@@ -10297,45 +10334,61 @@ var Engine = class {
10297
10334
  return { run_id: runId, output, outputs_by_node: outputs, stats, outputs_dir: writer.runDir, node_runs: nodeRuns };
10298
10335
  }
10299
10336
  async runLayers(canvas, graph, outputs, runId, writer, opts, counters, nodeRuns, needsBytes) {
10300
- const layers = topologicalLayers(graph);
10337
+ const order = topologicalLayers(graph).flat();
10301
10338
  const limit = resolveConcurrency(opts.concurrency);
10302
- for (const layer of layers) {
10303
- this.assertLayerMayRun(canvas, layer, opts);
10304
- const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
10305
- if (opts.signal?.aborted) {
10306
- return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
10307
- }
10308
- this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10309
- return this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then((r) => {
10310
- if (r.cached) counters.cachedNodes++;
10311
- counters.totalCredits += r.credits;
10312
- const node = canvas.nodes.find((n) => n.id === nodeId);
10313
- if (node) {
10314
- const run = {
10315
- node_id: nodeId,
10316
- node_type: node.type,
10317
- cached: r.cached,
10318
- duration_ms: r.durationMs,
10319
- credits: r.credits
10320
- };
10321
- nodeRuns.push(run);
10322
- this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10323
- }
10324
- });
10325
- });
10326
- const failures = [];
10327
- settled.forEach((result, i) => {
10328
- const nodeId = layer[i];
10329
- if (result.status === "rejected" && nodeId) {
10330
- if (result.reason instanceof RunAbortedError) return;
10331
- failures.push({ nodeId, reason: result.reason });
10332
- this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(result.reason) });
10339
+ if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before dispatch");
10340
+ const waitingOn = new Map(order.map((id) => [id, new Set(graph.get(id) ?? [])]));
10341
+ const running = /* @__PURE__ */ new Map();
10342
+ const failures = [];
10343
+ const held = [];
10344
+ const dispatch = (nodeId) => {
10345
+ waitingOn.delete(nodeId);
10346
+ this.emitProgress(opts, { kind: "node_start", node_id: nodeId });
10347
+ const settled = this.executeOne(canvas, nodeId, outputs, runId, writer, opts, needsBytes.has(nodeId)).then(
10348
+ (r) => {
10349
+ this.recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts);
10350
+ for (const deps of waitingOn.values()) deps.delete(nodeId);
10351
+ },
10352
+ (reason) => {
10353
+ failures.push({ nodeId, reason });
10354
+ this.emitProgress(opts, { kind: "node_failed", node_id: nodeId, error: failureDetail(reason) });
10333
10355
  }
10334
- });
10335
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted by signal");
10336
- if (failures.length === 1 && failures[0]) throw failures[0].reason;
10337
- if (failures.length > 1) throw new LayerExecutionError(failures);
10356
+ );
10357
+ running.set(
10358
+ nodeId,
10359
+ settled.finally(() => running.delete(nodeId))
10360
+ );
10361
+ };
10362
+ const isHeld = (nodeId) => heldByCheckpoint(canvas, [nodeId], opts.stop_before_kinds).length > 0;
10363
+ for (; ; ) {
10364
+ const ready = opts.signal?.aborted ? [] : order.filter((id) => waitingOn.get(id)?.size === 0);
10365
+ for (const nodeId of ready) {
10366
+ if (running.size >= limit) break;
10367
+ if (isHeld(nodeId)) {
10368
+ waitingOn.delete(nodeId);
10369
+ held.push(nodeId);
10370
+ } else dispatch(nodeId);
10371
+ }
10372
+ if (running.size === 0) break;
10373
+ await Promise.race(running.values());
10338
10374
  }
10375
+ throwRunOutcome(opts.signal, failures, held);
10376
+ }
10377
+ /** A settled node counted, recorded and announced. */
10378
+ recordRun(canvas, nodeId, r, counters, nodeRuns, outputs, opts) {
10379
+ if (r.cached) counters.cachedNodes++;
10380
+ counters.totalCredits += r.credits;
10381
+ const node = canvas.nodes.find((n) => n.id === nodeId);
10382
+ if (!node) return;
10383
+ const run = {
10384
+ node_id: nodeId,
10385
+ node_type: node.type,
10386
+ cached: r.cached,
10387
+ duration_ms: r.durationMs,
10388
+ credits: r.credits
10389
+ };
10390
+ nodeRuns.push(run);
10391
+ this.emitProgress(opts, { kind: "node_settled", run, outputs: outputs[nodeId] ?? {} });
10339
10392
  }
10340
10393
  /** Progress consumers are observers only — an exception there must never fail the run. */
10341
10394
  emitProgress(opts, event) {
@@ -10427,20 +10480,6 @@ var Engine = class {
10427
10480
  this.logOutputWarnings(node.id, outputsObj);
10428
10481
  return { cached: false, credits, durationMs: elapsed };
10429
10482
  }
10430
- /**
10431
- * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
10432
- * never leaves a layer half billed: a signal, the cost cap and a checkpoint.
10433
- */
10434
- assertLayerMayRun(canvas, layer, opts) {
10435
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
10436
- const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
10437
- if (held.length > 0) {
10438
- throw new RunAbortedError(
10439
- "checkpoint",
10440
- `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
10441
- );
10442
- }
10443
- }
10444
10483
  /** A cache hit replayed as this node's result: its outputs written, and free. */
10445
10484
  async replayCached(node, cacheKey, outputs, writer) {
10446
10485
  const cacheT0 = Date.now();
@@ -15710,7 +15749,7 @@ function contextItems(opts) {
15710
15749
  opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the lead line (the largest words) cannot be read. Judge only whether those words can be read, nothing else. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line (the largest words) would be unreadable on a phone feed. That is blocking."
15711
15750
  ],
15712
15751
  sizes: opts.formats.length > 1 ? [
15713
- "12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about (blocking). Rearranged for its shape is fine."
15752
+ `12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; start the finding with "SIZES:" and report it on the size that differs). Rearranged for its shape is fine.`
15714
15753
  ] : []
15715
15754
  };
15716
15755
  }
@@ -18488,6 +18527,9 @@ export {
18488
18527
  conceptSummary,
18489
18528
  conceptProblems,
18490
18529
  unwrittenProblems,
18530
+ sceneWordProblems,
18531
+ SPEC_SHAPES_HINT,
18532
+ adSpecErrorText,
18491
18533
  readAdSpec,
18492
18534
  withRenderedLettering,
18493
18535
  conceptClassOf,
@@ -18575,4 +18617,4 @@ export {
18575
18617
  defaultRegistry,
18576
18618
  createEngineFromEnv
18577
18619
  };
18578
- //# sourceMappingURL=chunk-GY352NRN.js.map
18620
+ //# sourceMappingURL=chunk-WMAHVZMY.js.map