@koda-sl/baker-cli 0.404.0 → 0.406.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5640,7 +5640,6 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
5640
5640
  | `--cache-policy <policy>` | `read_write` | `read_write`, `bypass`, or `read_only`. |
5641
5641
  | `--concurrency <n>` | `5` (or `BAKER_CANVAS_CONCURRENCY`) | Max nodes executing at once within a layer. |
5642
5642
  | `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Operator only. Company-scoped remote cache + durable asset persistence. |
5643
- | `--max-credits <n>` | uncapped (or `BAKER_CANVAS_MAX_CREDITS`) | Credit ceiling: aborts before billing when the estimate exceeds it, and at layer boundaries once actual spend does. Completed nodes stay cached, so retrying with a higher cap loses nothing. |
5644
5643
  | `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
5645
5644
  | `--until frames` | none | Stop before the clips with the frames settled; the run is recorded as `awaiting_approval` (not failed, not interrupted) and resumes with `--run-id`. |
5646
5645
  | `--cache-dir <path>` | `<workspace root>/canvas/.cache` | Operator only. Where content-addressed bytes + cache index live. |
@@ -5775,7 +5774,7 @@ It then scaffolds a canvas that draws **only what `prompt.json` holds**; no elem
5775
5774
 
5776
5775
  **Placement formats.** The canvas delivers the platform's full placement set, each format at its exact size, collected under `final` with the format as its label. The set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or `--placements none` for a single format. The report's `aspect_ratio` is the ratio the plates really render at (the model's nearest to the lead format), and `format_plan` says how each format is made.
5777
5776
 
5778
- **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed; its hints name the cheapest fix (overlapping or unreadable copy is fixed with a smaller `size`, a `highlight` or `sticker` treatment or shorter words, which re-draw the formats from the cached plates). **Render budget:** a static ad gets 3 renders per creative in a chat before the person reacts; the fourth exits 2 with `RENDER_BUDGET` without rendering, and `baker creatives feedback record` or `baker creatives select` on that creative opens a new round. Resuming an interrupted run does not count again, and a run that died only on an outage (timeout, network, a provider that says retry) gives its render back.
5777
+ **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed; its hints name the cheapest fix (overlapping or unreadable copy is fixed with a smaller `size`, a `highlight` or `sticker` treatment or shorter words, which re-draw the formats from the cached plates). **Render budget:** a static ad gets, per creative in a chat before the person reacts, 3 renders that paint pictures and then 4 re-draws from those pictures; a render that paints nothing (every billed node from the cache) counts as a re-draw. With the pictures spent a re-draw runs only from the cache (the engine's `billed_from_cache_only`), and one that would paint exits 2 with `RENDER_BUDGET` before billing; with both spent every render exits 2 with `RENDER_BUDGET` without rendering, and `baker creatives feedback record` or `baker creatives select` on that creative opens a new round. Resuming an interrupted run does not count again, and a run that died only on an outage (timeout, network, a provider that says retry) gives its render back.
5779
5778
 
5780
5779
  The logo is never a slot to fill: use the company's committed logo in `src/brand/logos/` (per `BRAND.md`), never `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark.
5781
5780
 
@@ -3894,8 +3894,12 @@ function estimateNodeCredits2(def, params) {
3894
3894
  if (!parsed.success) return 0;
3895
3895
  return priceNode(def, parsed.data)?.credits ?? 0;
3896
3896
  }
3897
- function estimateCanvasCredits(canvas, registry, ids) {
3898
- return canvas.nodes.filter((n) => !ids || ids.has(n.id)).map((n) => ({ node_id: n.id, node_type: n.type, credits: estimateNodeCredits2(registry.get(n.type), n.params) }));
3897
+ function estimateCanvasCredits(canvas, registry) {
3898
+ return canvas.nodes.map((n) => ({
3899
+ node_id: n.id,
3900
+ node_type: n.type,
3901
+ credits: estimateNodeCredits2(registry.get(n.type), n.params)
3902
+ }));
3899
3903
  }
3900
3904
  function sumCredits(perNode) {
3901
3905
  return perNode.reduce((total, n) => total + n.credits, 0);
@@ -4270,16 +4274,6 @@ function dfsCycle(u, color, stack, reverseAdj) {
4270
4274
  stack.pop();
4271
4275
  return null;
4272
4276
  }
4273
- function nodesBeforeCheckpoint(graph, types, stopKinds) {
4274
- const all = new Set(graph.keys());
4275
- if (!stopKinds?.size) return all;
4276
- const running = /* @__PURE__ */ new Set();
4277
- for (const layer of topologicalLayers(graph)) {
4278
- if (layer.some((id) => stopKinds.has(types.get(id) ?? ""))) return running;
4279
- for (const id of layer) running.add(id);
4280
- }
4281
- return running;
4282
- }
4283
4277
 
4284
4278
  // src/engine/engine/validator.ts
4285
4279
  import path5 from "path";
@@ -6153,12 +6147,6 @@ var Engine = class {
6153
6147
  async run(input, opts = {}) {
6154
6148
  const validation = await this.validateDeep(input);
6155
6149
  if (!validation.ok) throw new ValidationError(validation.issues);
6156
- if (!opts.stop_before_kinds?.size && opts.max_credits !== void 0 && validation.estimatedCredits > opts.max_credits) {
6157
- throw new RunAbortedError(
6158
- "cost_cap",
6159
- `estimated ${validation.estimatedCredits} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
6160
- );
6161
- }
6162
6150
  const canvas = validation.canvas;
6163
6151
  const runId = opts.run_id ?? `r_${ulid()}`;
6164
6152
  const writer = new OutputWriter({ outputsDir: this.outputsDir, runId });
@@ -6172,20 +6160,6 @@ var Engine = class {
6172
6160
  const counters = { cachedNodes: 0, totalCredits: 0 };
6173
6161
  const nodeRuns = [];
6174
6162
  const graph = this.pruneToOutput(canvas, buildDependencyGraph(canvas));
6175
- if (opts.max_credits !== void 0 && opts.stop_before_kinds?.size) {
6176
- const willRun = nodesBeforeCheckpoint(
6177
- graph,
6178
- new Map(canvas.nodes.map((n) => [n.id, n.type])),
6179
- opts.stop_before_kinds
6180
- );
6181
- const estimate = sumCredits(estimateCanvasCredits(canvas, this.registry, willRun));
6182
- if (estimate > opts.max_credits) {
6183
- throw new RunAbortedError(
6184
- "cost_cap",
6185
- `estimated ${estimate} credits exceeds the ${opts.max_credits}-credit cap \u2014 nothing was billed; raise --max-credits or shrink the canvas`
6186
- );
6187
- }
6188
- }
6189
6163
  const needsBytes = computeNeedsLocalBytes(canvas, graph, this.registry);
6190
6164
  this.emitProgress(opts, {
6191
6165
  kind: "plan",
@@ -6223,20 +6197,7 @@ var Engine = class {
6223
6197
  const layers = topologicalLayers(graph);
6224
6198
  const limit = resolveConcurrency(opts.concurrency);
6225
6199
  for (const layer of layers) {
6226
- if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
6227
- if (opts.max_credits !== void 0 && counters.totalCredits > opts.max_credits) {
6228
- throw new RunAbortedError(
6229
- "cost_cap",
6230
- `spent ${counters.totalCredits} credits, over the ${opts.max_credits}-credit cap \u2014 completed nodes are cached; raise --max-credits to continue where this stopped`
6231
- );
6232
- }
6233
- const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
6234
- if (held.length > 0) {
6235
- throw new RunAbortedError(
6236
- "checkpoint",
6237
- `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
6238
- );
6239
- }
6200
+ await this.assertLayerMayRun(canvas, layer, outputs, runId, opts);
6240
6201
  const settled = await mapWithConcurrency(layer, limit, (nodeId) => {
6241
6202
  if (opts.signal?.aborted) {
6242
6203
  return Promise.reject(new RunAbortedError("signal", "run aborted before node dispatch"));
@@ -6363,6 +6324,53 @@ var Engine = class {
6363
6324
  this.logOutputWarnings(node.id, outputsObj);
6364
6325
  return { cached: false, credits, durationMs: elapsed };
6365
6326
  }
6327
+ /**
6328
+ * Every reason a layer must not dispatch, checked before any of its nodes starts, so a stop
6329
+ * never leaves a layer half billed: a signal, the cost cap, a checkpoint, and a billed node
6330
+ * the cache cannot serve when the run is cache-only.
6331
+ */
6332
+ async assertLayerMayRun(canvas, layer, outputs, runId, opts) {
6333
+ if (opts.signal?.aborted) throw new RunAbortedError("signal", "run aborted before layer dispatch");
6334
+ const held = heldByCheckpoint(canvas, layer, opts.stop_before_kinds);
6335
+ if (held.length > 0) {
6336
+ throw new RunAbortedError(
6337
+ "checkpoint",
6338
+ `stopped before ${held.join(", ")} \u2014 everything up to here is done and cached, so continuing this run id pays for none of it again`
6339
+ );
6340
+ }
6341
+ if (opts.billed_from_cache_only) {
6342
+ const fresh = await this.billedMisses(canvas, layer, outputs, runId, opts);
6343
+ if (fresh.length > 0) {
6344
+ throw new RunAbortedError(
6345
+ "cache_only",
6346
+ `${fresh.join(", ")} would be rendered fresh and billed \u2014 nothing in this layer ran`
6347
+ );
6348
+ }
6349
+ }
6350
+ }
6351
+ /**
6352
+ * The billed nodes of a layer that would miss the cache, found before any of them runs:
6353
+ * each is prepared exactly as `executeOne` would (same inputs, params and cache key).
6354
+ *
6355
+ * `executeOne` prepares each node again when it runs, so a cache-only layer hydrates its
6356
+ * text slots and hashes its `cacheKeyExtras` twice. Deliberate: a re-draw's layers are
6357
+ * small, and threading this preparation into `executeOne` would couple the check to the
6358
+ * run path, where a stale preparation would bill the wrong key.
6359
+ */
6360
+ async billedMisses(canvas, layer, outputs, runId, opts) {
6361
+ const misses = [];
6362
+ for (const nodeId of layer) {
6363
+ const node = canvas.nodes.find((n) => n.id === nodeId);
6364
+ const def = node ? this.registry.get(node.type) : void 0;
6365
+ if (!node || !def) continue;
6366
+ const token = resolveRegenerateToken(node, opts.regenerate, runId);
6367
+ const prepared = await prepareForExecution(node, outputs, def, canvas.cache_salt, token, this.assets);
6368
+ if (estimateNodeCredits2(def, prepared.renderedParams) <= 0) continue;
6369
+ const hit = opts.cache_policy === "bypass" ? null : await this.cache.get(prepared.cacheKey);
6370
+ if (!hit) misses.push(nodeId);
6371
+ }
6372
+ return misses;
6373
+ }
6366
6374
  /** A cache hit replayed as this node's result: its outputs written, and free. */
6367
6375
  async replayCached(node, cacheKey, outputs, writer) {
6368
6376
  const cacheT0 = Date.now();
@@ -11668,4 +11676,4 @@ export {
11668
11676
  defaultRegistry,
11669
11677
  createEngineFromEnv
11670
11678
  };
11671
- //# sourceMappingURL=chunk-KE6GXLDE.js.map
11679
+ //# sourceMappingURL=chunk-YOF4XTUW.js.map