@koda-sl/baker-cli 0.263.0-dev.ac71c8123 → 0.264.0-dev.141d81fad

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.
Files changed (29) hide show
  1. package/README.md +52 -16
  2. package/canvas/tiktok-captions-composition/index.html +2 -19
  3. package/dist/{chunk-GXRUZ4LP.js → chunk-2IB3IL4K.js} +2 -2
  4. package/dist/{chunk-TBEVU7XF.js → chunk-6F52WYB7.js} +3 -3
  5. package/dist/{chunk-ZK5S7JV2.js → chunk-77OP3PDC.js} +37 -155
  6. package/dist/chunk-77OP3PDC.js.map +1 -0
  7. package/dist/{chunk-POYXIDOD.js → chunk-DZUVUGEP.js} +1 -5
  8. package/dist/{chunk-POYXIDOD.js.map → chunk-DZUVUGEP.js.map} +1 -1
  9. package/dist/{chunk-ECPNYXOA.js → chunk-GU7IWEBC.js} +12 -5
  10. package/dist/chunk-GU7IWEBC.js.map +1 -0
  11. package/dist/{chunk-2VM2J6MB.js → chunk-KSOGK7SJ.js} +4 -4
  12. package/dist/cli.js +7293 -5477
  13. package/dist/cli.js.map +1 -1
  14. package/dist/client-VDCCDEHE.js +17 -0
  15. package/dist/engine/index.js +3 -3
  16. package/dist/{env-HZNFGXAR.js → env-FWMZXMQK.js} +2 -4
  17. package/dist/{output-MWMAXN4M.js → output-JBYI5IVX.js} +5 -5
  18. package/dist/{shared-NFLXWGHK.js → shared-GWFXJXKJ.js} +6 -6
  19. package/package.json +1 -1
  20. package/dist/chunk-ECPNYXOA.js.map +0 -1
  21. package/dist/chunk-ZK5S7JV2.js.map +0 -1
  22. package/dist/client-7QIDRQXG.js +0 -15
  23. /package/dist/{chunk-GXRUZ4LP.js.map → chunk-2IB3IL4K.js.map} +0 -0
  24. /package/dist/{chunk-TBEVU7XF.js.map → chunk-6F52WYB7.js.map} +0 -0
  25. /package/dist/{chunk-2VM2J6MB.js.map → chunk-KSOGK7SJ.js.map} +0 -0
  26. /package/dist/{client-7QIDRQXG.js.map → client-VDCCDEHE.js.map} +0 -0
  27. /package/dist/{env-HZNFGXAR.js.map → env-FWMZXMQK.js.map} +0 -0
  28. /package/dist/{output-MWMAXN4M.js.map → output-JBYI5IVX.js.map} +0 -0
  29. /package/dist/{shared-NFLXWGHK.js.map → shared-GWFXJXKJ.js.map} +0 -0
package/README.md CHANGED
@@ -226,6 +226,11 @@ baker ads google query "SELECT campaign.name, metrics.clicks, metrics.cost_micro
226
226
  # Use a preset (saves tokens) — returns only actually-serving entities by default
227
227
  baker ads google query --preset campaign-performance --customer-id 1234567890
228
228
 
229
+ # Campaigns Google reports as limited by budget, with each one's bid target next to its
230
+ # actual cost per conversion. Since 17 Aug 2026 a budget-limited campaign delivers toward
231
+ # its target instead of holding its recent actual cost, so a target above actual raises cost.
232
+ baker ads google query --preset budget-limited --customer-id 1234567890
233
+
229
234
  # Include paused entities too (default is serving-only)
230
235
  baker ads google query --preset ad-copy-performance --customer-id 1234567890 --include-paused
231
236
 
@@ -653,17 +658,21 @@ baker tag-manager draft show --chat <chat-id> gtm_temp_ab12
653
658
  baker actions draft --chat <chat-id>
654
659
  ```
655
660
 
656
- Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--max-cpc` take major units; `--target-roas` a ratio. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file <payload.json>` (flags override file keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
661
+ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remove), `negative-keywords`, `keyword-lists`, `ads`, `assets` (create/update/attach/detach), `asset-groups` (create/update/attach/detach — Performance Max), `audiences`, `conversions` (create/update/goal/upload), `bidding-strategies`, `labels`, `campaign-criteria` (add/update/remove), and `draft`. Amounts are in major currency units (converted to micros). Money/bids: `--amount`, `--cpc-bid`, `--target-cpa`, `--target-cpc`, `--max-cpc` take major units; `--target-roas` a ratio. `--target-cpc` pairs with `--bidding-strategy TARGET_CPC` and is Demand Gen only. `--max-cpc` sets the max CPC bid ceiling for `TARGET_IMPRESSION_SHARE` / `TARGET_SPEND` / `PERCENT_CPC` and must be paired with `--bidding-strategy`. Less-common ops accept a `--file` payload — a path to a JSON file, or the JSON object written inline (flags override its keys). Updates target a resource name or bare id as the positional argument; a target that names an op staged earlier **amends it in place**.
657
662
 
658
663
  **A device criterion can be added, but never removed** — `campaign-criteria add` with a device criterion works, but Google then rejects `campaign-criteria remove` on it forever, including on a criterion you added yourself (and Baker refuses the removal before publish). Adjust it instead with `campaign-criteria update customers/<cid>/campaignCriteria/<campaignId>~<criterionId> --bid-modifier <n>`: `1` for no adjustment, `1.25` to bid 25% more, `0.9` to bid 10% less, `0` to stop serving on that device. Locations, languages and ad schedules remove normally.
659
664
 
665
+ **Images on a Search campaign** — `assets create` with an image publishes it into the advertiser's Google Ads asset library, but Google refuses the asset link that puts an image extension on a Search campaign (`AD_IMAGE` and every marketing-image field type, at campaign, ad group and customer level alike), so `assets attach` rejects it at stage time and names what to do instead. Staging an image therefore also stages a **Task** — "Add N images to <campaign> in Google Ads" — which applies with the rest of the chat, so the one step Google keeps inside its own interface is tracked rather than mentioned. Attach the image to a Performance Max asset group (`asset-groups attach`) and the Task withdraws itself. The one image a Search campaign does take through a link is the advertiser's logo: `BUSINESS_LOGO`, at campaign or customer level, on a verified account.
666
+
660
667
  **Primary vs secondary conversion actions** — `conversions update <id> --primary` makes an action a primary action (automated bidding optimizes toward it); `--no-primary` (or `--primary=false`) demotes it to secondary (reported only) — the spaced form `--primary false` is rejected, since a boolean flag written with a space is set to true and the word dropped. `conversions create` stages a new action as **secondary** unless its `--file` payload sets `"primaryForGoal": true`, so a new action never silently joins the bidding target of a Maximize Conversions or Target CPA campaign. The staged card shows an "Action optimization" row with the before/after role.
661
668
 
662
669
  **Account-level conversion goals** — `conversions goal --customer-id … --category PURCHASE --origin WEBSITE --biddable` sets Google's `CustomerConversionGoal` for one (category, origin) pair; `--no-biddable` (or `--biddable=false`) excludes the category from bidding while it keeps being measured. Neither flag defaults, and the spaced form `--biddable false` is rejected. A conversion drives automated bidding only when both halves are on — the action is primary AND its category's account-level goal is biddable — so this is where "my conversion is Primary but bidding ignores it" is fixed. One staged op per pair.
663
670
 
671
+ **Uploading conversions Google never received** — `conversions upload --customer-id 1234567890 --conversion-action 987654321 --file missing.csv` files conversions the account has no record of, for a period when the tracking was broken. The file is a CSV, JSON or one-object-per-line export whose header names the columns `orderId`, `conversionDateTime`, `gclid` / `gbraid` / `wbraid`, `email`, `phone`, `value`, `currency`; column names are matched loosely (`Order ID`, `order_id`, `transaction_id` are one column) and any column an upload has no use for is ignored and listed back in the hints. A single conversion can be given with `--order-id --date --gclid --email --value --currency` instead. Each conversion needs an order id, a date **with its timezone** (a bare `2026-08-01T10:00:00` is refused, since it means one instant to the person who typed it and another to everything else), and something to match on — a click id or the customer's email / phone. Reuse each conversion's **original** order id: that is what makes a re-send a correction rather than a second conversion. A value always carries its currency, because Google reads one without in the account's own. Emails and phone numbers are hashed at stage time, so nothing identifying is stored, sent in the clear, or kept afterwards. Staging checks the conversion action exists and is not removed (either refuses), and warns about conversions older than that action's click-through window — Google accepts those and attributes them to nothing. Like every other Google write it applies on publish; more than 200 conversions stage as several uploads that publish independently.
672
+
664
673
  **Retiring a conversion action** — `status` accepts `ENABLED` and `REMOVED` only. Google denies `HIDDEN` on every conversion-action write, so to stop bidding toward an action while it keeps measuring use `--no-primary`, and to stop it counting at all use `"status": "REMOVED"`. Conversion actions **Google owns** cannot be changed at all — Smart campaign call/directions actions, the in-ad lead form's submit, YouTube subscriptions and follow-on views, Floodlight and Search Ads 360 imports, store visits, codeless webpage actions. Staging any edit to one, a rename included, is refused up front; there is no manual route either, since the Google Ads interface will not change them.
665
674
 
666
- **Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
675
+ **Reviewing the draft** — `baker ads google draft list` renders everything you've staged as a grouped campaign ▸ ad group ▸ ad tree (with non-blocking completeness advisories), the CLI counterpart to the dashboard's Google Ads tab. Pass `--json` for the raw envelope. Once the chat has published it reports the outcome instead — `published · N applied, M failed`, with a `✗` line naming each op that did not land and why. Aim for a fully built campaign — 2–4 ad groups, ≥5 keywords each, 2–4 RSAs with 8–12 headlines, ≥4 sitelinks / ≥3 callouts / ≥1 structured snippet, and ≥1 shared negative list; the advisories flag what's still thin.
667
676
 
668
677
  **Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
669
678
 
@@ -1063,6 +1072,8 @@ audit --format md # deliverable-ready markdown table
1063
1072
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1064
1073
  campaigns create|update|pause|resume|archive|duplicate
1065
1074
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1075
+ account url-params # UTMs for every ad in the whole account
1076
+ url-params [--ad-sets] # read what is tagged now, and what lands twice
1066
1077
  creatives create|update|pause|resume|duplicate
1067
1078
  audiences create|upload
1068
1079
  conversions create|update
@@ -1090,6 +1101,10 @@ baker ads linkedin campaigns url-params 123456 \
1090
1101
  --param "utm_source=linkedin&utm_medium=paid-social" \
1091
1102
  --dynamic utm_campaign=CAMPAIGN_NAME --dynamic utm_content=CREATIVE_ID
1092
1103
  baker ads linkedin campaigns url-params 123456 --clear # remove them all
1104
+
1105
+ # what never varies across the account goes one level up — LinkedIn appends BOTH,
1106
+ # so a key set at the account AND the ad set lands in the URL twice
1107
+ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=paid_social"
1093
1108
  # a boolean flag written with a space (`--clear false`) is rejected: citty sets it to TRUE and drops
1094
1109
  # the word. Attach it — `--clear=false` — or leave the flag off.
1095
1110
 
@@ -2729,6 +2744,15 @@ When `list` or `search` returns nothing, the envelope carries `meta.empty` expla
2729
2744
 
2730
2745
  If the source check itself fails, `meta.empty` is omitted entirely and the hint says so — an unverified absence is never reported as one.
2731
2746
 
2747
+ `list` is paged. `--limit` is the page size (default 50); when the library continues past the page you got, the envelope carries `meta.next_cursor` and a hint, and `meta.empty` is omitted — a page that is not the last page establishes nothing about the corpus. Read on with the same filters plus `--cursor`:
2748
+
2749
+ ```bash
2750
+ baker testimonials list --sentiment positive --limit 50
2751
+ baker testimonials list --sentiment positive --limit 50 --cursor <meta.next_cursor>
2752
+ ```
2753
+
2754
+ No `meta.next_cursor` means the library ended there.
2755
+
2732
2756
  ### `baker testimonials tags`
2733
2757
 
2734
2758
  List the available testimonial tag names — built-in defaults plus the company's custom tags. Use it before filtering with `--tags`. Defaults to a markdown list (`--output json` for the `{ ok, data }` envelope).
@@ -3004,7 +3028,7 @@ baker scheduled-actions create --template baker-ads --name "Weekly ad review" --
3004
3028
  Rules:
3005
3029
 
3006
3030
  - `--cron` and `--run-at` are mutually exclusive.
3007
- - `--run-at` must be an ISO UTC timestamp ending in `Z`.
3031
+ - `--run-at` must be an ISO UTC timestamp ending in `Z`, and still in the future — a time that has already passed is refused when the op is staged. If a one-off's time passes while the chat is still open, publishing creates the task **switched off** with the reason on it rather than dropping it, so it is never lost.
3008
3032
  - Draft-created ids use `temp_sched_*`; use them with `get`, `update`, or `delete` before publish.
3009
3033
  - Staged commands require `BAKER_CHAT_ID` to point to an `in_progress` chat.
3010
3034
  - `trigger` rejects `temp_sched_*` and only accepts published scheduled-action ids.
@@ -3480,7 +3504,6 @@ Reach for it when you are relaying the **client's** own words. When you are writ
3480
3504
 
3481
3505
  ```bash
3482
3506
  baker studio animate "slow push in, model turns to camera and smiles" --image j57abc123def456ghi789
3483
- baker studio animate "she looks to camera and says: \"Hola, soy Elena\"" --avatar elena --quality 720p --aspect-ratio 9:16
3484
3507
  baker studio animate "handheld drift right, steam rising from the cup" \
3485
3508
  --image ./out/hero.png --duration 6 --quality 1080p
3486
3509
  baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
@@ -5791,18 +5814,7 @@ This CLI is designed for AI agent consumption. Key patterns:
5791
5814
 
5792
5815
  - **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
5793
5816
 
5794
- - **0.250.1**: `--avatar <handle>` on `baker studio generate` and `baker studio animate` never worked. Both were declared in `registerSchema` and read at the call site, but neither was in the command's own citty `args` — and citty does not reject an undeclared flag, it keeps it and sets it to `true`. So `--avatar elena` reached the route as `avatar: true` with `elena` dropped as a stray positional, and the body schema (`z.string()`) refused it. Documented since 0.217.0, broken since 0.217.0: casting an avatar into a render was not possible from the CLI at all, which is why a clip of a named presenter came back with neither the identity sheet nor the voice. Both flags are now declared. A new test in `command-reference-integrity` compares every registered schema against the args of the command that parses it, so the two cannot drift again — it found a third case on the way, `actions status`, whose schema advertised a `--refs` flag for what the command actually takes as positionals.
5795
- - **0.251.0**: `--accent` on `baker avatars create|update` — the region an avatar's voice comes from, e.g. `"Spanish from Spain, Madrid accent"`. Its own field rather than a sentence inside `--speech`, because that is where it went to die: asked for "pace, register, accent", the agent wrote *"clear Spanish accent"* for a Madrid presenter, the video model read that as no accent, and the clip came out sounding like nowhere. A place can be checked; an adjective cannot. It reaches the render through `buildDeliveryDirection` as its own `ACCENT:` instruction, and `thinProfileHint` now names it first among the missing fields since it is the only one that fails silently — a clip with no accent still looks finished.
5796
- - **0.252.0**: avatar voices removed. `--voice-id` / `--voice-description` are gone from `baker avatars create|update`, and an avatar no longer holds a voice at all — every clip is voiced by the video model as it renders, directed by the profile's `--accent`. The picker they replaced offered a voice, played its preview, and then shipped a clip that did not sound like it: the re-voice was speech-to-speech, which replaces timbre and keeps pronunciation, so the accent the user picked could never survive. Three other routes were built and judged on video before removing it — TTS + lip-sync (mouth), audio-driven avatar models (picture), and Seedance 2.5 driven by audio (refuses AI faces). Reasoning and the re-open condition: `docs/adr/0005-an-avatar-is-a-face-not-a-voice.md`.
5797
- - **0.253.0**: the pre-render gate runs `hyperframes check` — lint, runtime, layout, motion and contrast in one browser session, sampled at transition seams — instead of the static `lint` + `inspect` pair, falling back to them when the installed binary predates `check`. The browser half is signal the parser could never produce: measured on HyperFrames' own `warm-grain` example, lint/runtime/layout/motion all reported `ok: true` while the composition rendered an almost entirely blank ten-second video, the only trace being six `GSAP target #a-roll not found` warnings — a tween pointed at an element that does not exist, so nothing it animates ever appears. Baker promotes that warning to blocking, alongside `sweep_static` ("the timeline never advanced", i.e. a still image billed as a video). Warnings are grouped by code with a count (one run returned 62, 53 of them identical) and the highest-value findings carry advice on what to do rather than only what was seen. Requires `hyperframes@0.8.17` in the sandbox, bumped from 0.7.5 in the same change. Reasoning: `docs/adr/0007-the-render-gate-runs-in-a-browser.md`.
5798
- - **0.254.0**: the nested-composition smoke test now looks at the frames instead of trusting the exit code — exiting 0 is what a blank render does too. `hyperframes snapshot --describe` sends the captured frames to Gemini and writes back what is actually in them, and the credential Studio already uses for Gemini and Omni (`GOOGLE_GENERATIVE_AI_API_KEY`) is the `GEMINI_API_KEY` it reads; the backend now ships it to the sandbox by omission, so a deployment without one keeps the plain smoke test. Verified against a real render: the pass reported "the image is completely empty" and named the single visible element without being told what to look for. Frame verdicts are condensed into the node log, one line per frame — a file the agent would have to know to open is a file it does not read.
5799
- - **0.263.0**: shot briefs refuse the frame that contradicts its own caption, and the vision pass reads the numbers back. Observed verbatim: a phone screen reading "$215.40 SAVED THIS MONTH" under a caption reading "Save over £900 a year" — wrong currency, and a figure that contradicts the claim it sits beneath, since 215.40 a month is 2,584 a year. The model invents a number whenever a screen or document is in shot, so the frames no longer carry legible text at all: the caption track is where an ad's numbers live. Same brief also pins each object's real-world FORM after a phone rendered with a second screen on its back, and names the market's currency for anything unavoidably visible. The frame-vision question now reads out every number and word inside the picture and says whether it contradicts the caption — the defect only exists between the two, so neither alone can see it.
5800
- - **0.262.0**: `scaffold-ad` carries the brand. The spec takes `brand.palette` (hex tokens) and `brand.logo` (repo path to the mark), and emits them as the `style.palette` and `branding` the engine already reads to colour its brand plate and CTA. The palette dresses the plate and the type and is deliberately kept OUT of the shot briefs — hex in a frame prompt makes the model paint the whole scene that colour, which reads as a filter. Also takes `market`, since an ad that does not say where it is set is an ad the model sets wherever it likes.
5801
- - **0.261.0**: four defects that reached a finished ad, fixed in `scaffold-ad` where they originate. Frames render on **`openai/gpt-image-2`**, pinned rather than inheriting the catalogue default (Gemini Flash — right for cheap high-volume work, wrong for the opening still a five-second clip is built from). Every shot brief now states the MARKET, inferred from the voice language when unstated: an ad for Spain came back full of British houses because nothing said where it was. Every shot brief also rules out the impossible — paper and screens are opaque, objects are at real-world scale — after a bill rendered with the text from its reverse showing through, and a solar panel five metres tall. And beat durations snap to lengths the video model can actually render: free-form durations were rounded down per clip, so 11 beats summed to 27.0s of picture against a 31.4s timeline and the 4.4s difference rendered as a black tail. The read rate was re-measured off a real Spanish ad (81 words in 26.48s = 3.06 w/s); the old 2.6 over-predicted every beat by 18%.
5802
- - **0.260.0**: burned-in captions break where the script punctuates instead of every N words. The karaoke composition sliced the transcript into fixed groups, and a word count knows nothing about the sentence it is cutting — an ad the engine rendered showed cards reading "OTRA VEZ, EN" and "TU CASA GENERA", half-thoughts ending on a preposition. A card now closes when the clause does; the word count stays as a ceiling so a long clause is still split, and a linking word ("de", "al", "y") never ends a card because it belongs to what follows. Same rule the pre-render gate already checked for, now enforced where the cards are actually built.
5803
- - **0.259.0**: `baker canvas scaffold-ad` is listed in `baker canvas --help` with a description, and the three video routes are separated in the skill. It had a usage line and no description row, so an agent reading help saw two scaffolders explained and a bare name — and three runs from the same prompt took three different wrong paths: hand-directing the flow, reproducing a competitor ad found via `winning-ads`, and hand-authoring a canvas. The routes now state their own boundary: `scaffold-ad` is the default for a new ad, `scaffold-video` only reproduces a video the USER supplied, and `video-flow.md` opens by saying so rather than calling itself the default eleven times.
5804
- - **0.257.0**: the frame-vision pass now asks whether what is on screen could physically happen, not just whether the frame is empty. The defect that motivated it was a generated shot of a solar panel roughly five metres tall being lifted onto a roof by one person — well lit, on brief, perfectly legible, and impossible. That is what reads loudest as "AI-generated", and the previous question could not see it: nothing was missing, something present was the wrong size. Asked the new question, the pass called the rejected shot *"physically implausible due to the immense weight and surface area"* and the replacement *"consistent with what two people could reasonably maneuver"* — the same verdict a person reached, unprompted.
5805
- - **0.256.0**: the pre-render gate reports caption cards that end mid-clause. The first cut of this check asked for consistency and got it the wrong way round — stripping terminal punctuation makes the cards agree and leaves them wrong, because "En solo nueve días tu casa" is not a line anyone wrote, it is a sentence halved by a word count. Captions split where the script punctuates; each card is then a clause carrying its own mark, grammatical, and consistent as a by-product. The last card is exempt, since a CTA legitimately ends bare. Reported, never rewritten — the copy belongs to whoever wrote it.
5817
+ ## Publishing
5806
5818
 
5807
5819
  ### Auto-publish (CI)
5808
5820
 
@@ -5815,6 +5827,30 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
5815
5827
  ./scripts/publish-package.sh cli next # Publish as @next (pre-release)
5816
5828
  ```
5817
5829
 
5830
+ - **0.243.0**: `baker ads linkedin account url-params` stages the URL tracking parameters on the whole **ad account** — the "Account tracking parameters" box in Campaign Manager, which the CLI (and this README, and the playbook) said was UI-only and unreachable through the API. It was, until LinkedIn's 202608 added `sponsoredAccount` to the `adEntity` key of `/adTrackingParameters`; Baker's LinkedIn version pin moves 202606 → 202608 with it. Same flags as the ad-set form (`--param`, `--dynamic`, `--clear`, `--file`) and no positional: the target is built from `--account-id`, because a bare numeric id has always meant an ad set and reading one as an account would silently stage the wrong write. **LinkedIn concatenates the two levels rather than overriding**, so a key set at both appears twice in the served URL — the stage warnings now name the other level for exactly that check, and the staged change says which level it applies to. `baker ads linkedin account` keeps reading account detail unchanged (it is now `account detail`, kept as the default subcommand).
5831
+
5832
+ - **0.244.0**: `baker ads linkedin url-params` **reads** the tracking parameters in effect — the ad account's, optionally every ad set's, and the keys set at BOTH levels. There was no read at all before: the only way to see what an entity carried was to stage a change and look at the before-state, which meant the concatenation trap (a key at the account AND an ad set lands in the served URL twice) was invisible until a report came back split across two values of the same parameter. Account-only by default because `adTrackingParameters` has no batch read, so each ad set is its own call; `--ad-sets` sweeps them, capped at 50, and the response carries `adSetCoverage: {examined, total, capped}` so a partial sweep can't read as a clean account. `--collisions-only` for just the finding.
5833
+
5834
+ - **0.245.0**: **a repeated flag no longer loses everything but its last value.** citty declares no array arg type — it calls `util.parseArgs` without `multiple: true` — so `--param utm_source=linkedin --param utm_medium=paid` reached the command as the single string `utm_medium=paid`. The arg descriptions said "repeatable", the unit tests passed arrays citty never produces, and `url-params` REPLACES an entity's whole parameter set: staging three UTMs set one and deleted the rest at publish, reporting success throughout. Found by staging against the real ad account and reading the payload back, not by any test. Every repeatable flag now reads its values off the command line: LinkedIn `--param`/`--dynamic`, Google Ads `--custom-param` (Google replaces the set wholesale too) and `--text`, `baker mcp add --header`, and `baker flows map --map`, which had already solved this locally and is now the shared helper.
5835
+
5836
+ - **0.245.0**: `baker ads linkedin url-params --campaign-ids` says when an id names no ad set. LinkedIn answers NOT_FOUND identically for "this ad set carries no parameters" and "there is no such ad set", so a mistyped id came back as an ad set that exists and is empty — and the next move on that is a write that only fails at publish. Such ids are now reported under `unreadable` with the reason, and the "nothing is tagged" hint is suppressed, exactly as for a level that could not be read.
5837
+
5838
+ - **0.245.0**: a backend condition the caller can act on reaches the caller. "Analytics reporting is not switched on for this workspace yet" was thrown as an `INTERNAL`, and the HTTP layer withholds the message of every 5xx so a real fault cannot leak its internals — so the CLI printed `INTERNAL_ERROR: Internal server error` and, worse, LinkedIn and Meta commands marked it retryable. It is now a `CONFLICT`, which the CLI maps to a new non-retryable `CONFLICT` code and prints verbatim.
5839
+
5840
+ - **0.246.0**: `baker ads linkedin draft` reports what a publish DID. It used to render a published draft exactly like a staged one — same "1 staged change", same tree, no mention of the outcome — even though the per-op `result` was in the response the whole time. So the one surface an agent reaches for to check a publish could not answer, and a publish that changed LinkedIn and then failed to finish looked identical to one that had not run yet. It now reads `published · 1 applied` / `2 applied, 1 failed`, names each failed or skipped op with its error, and says `publishing` while one is still in flight. Found because a tracking-parameter publish did exactly that: applied at LinkedIn, then left the draft stuck, with the only trace an `Error calling onComplete` in the server log.
5841
+
5842
+ - **0.247.0**: a tracking-parameter write with no `--dynamic` no longer fails at publish. LinkedIn's `/adTrackingParameters` record requires BOTH maps and rejects a body missing either — `ERROR :: /dynamicValueParameters :: field is required but not found and has no default value` — so the most ordinary write there is, a couple of fixed UTMs and no placeholder, was a hard failure, while every write that happened to carry a `--dynamic` succeeded. Both maps are now always sent, empty when unused. Found by the 0.246.0 outcome line, on the first real publish that did not pass `--dynamic`.
5843
+
5844
+ - **0.248.0**: a tracking-parameter read that fails no longer reads as "there is nothing there". The idempotency token is LinkedIn's concurrency guard AND the only look at the before-state, and `url-params` REPLACES the entity's whole set — so writing without it is not a degraded write, it is an unguarded one that overwrites live parameters having never seen them. Both readers collapsed every failure into "none set": the executor logged `not readable; writing as a fresh set` and did exactly that, and the stage-time snapshot logged `previewing as none set`, which silently removed the drop warning that exists to catch this. Only LinkedIn's NOT_FOUND now means "none yet" (a first write — and no longer logged, since it is the normal case that was filling the log). Any other failure fails the publish, and at stage time produces a warning saying the current parameters could not be read rather than none being there. It matters most on the 409 retry: a 409 means somebody changed the parameters between our read and our write, and a swallowed failure on the re-read turned that detected conflict into a blind overwrite of their change.
5845
+
5846
+ Reading it back changes three things beyond the command. **`campaigns url-params` / `account url-params` now warn by name when a write would delete a live key** — LinkedIn replaces an entity's whole parameter set rather than merging into it, so staging one pair on an ad set that already carries three published as "one pair, and the other three are gone". Pairs accumulate inside a chat, which is exactly what made a merge at publish the natural expectation; the stage warning names what would be dropped, and the guidance now says to read first and re-send everything that should survive. **`baker analytics map` can check a proposed LinkedIn mapping against the account** — LinkedIn was on the list of platforms with no configuration read, so its mappings shipped with no evidence behind them; it now reads the account level and up to ten ad sets. And **the `tracking.campaign_parameters` audit check answers before a campaign runs**: with no traffic it used to be `n_a`, honest but useless at the one moment the fix is free, and it now judges the account's own parameters — with wording that says so rather than claiming clicks arrived carrying nothing when no click arrived.
5847
+
5848
+ Three more things the 202608 pin brought with it. **The conversion-type enum went from 8 values to all 34 LinkedIn accepts** — `QUALIFIED_LEAD`, `BOOK_APPOINTMENT`, `SUBMIT_APPLICATION` and 23 others were rejected by our own schema before the request ever left, and 202608 adds `MARKETING_QUALIFIED_LEAD` / `SALES_QUALIFIED_LEAD` on top. **Attribution windows gained 365** (and view-through gained 90), with the type-conditional rule enforced on `conversions create`: 365 is only legal for `SUBMIT_APPLICATION`, `PURCHASE`, `ADD_TO_CART`, `QUALIFIED_LEAD`, `LEAD`. And **a 429 carrying `SEGMENT_LIMIT_EXCEEDED` is no longer reported as a throttle** — 202608 caps an ad account at 1,000 DMP segments and reuses the rate-limit status for it, so an agent that read it as "retry in a second" would have retried forever against a cap only a human can clear in Campaign Manager.
5849
+
5850
+ - **0.256.0**: **`assets attach` no longer offers an image link, because Google accepts none.** A production publish came back with six `AssetLinkError.FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE` rows (trigger `MARKETING_IMAGE,SEARCH`) on an otherwise built and live Search campaign — staged exactly as this CLI's own advisory and the scaffold's ads-google doc instructed, at `"fieldType": "MARKETING_IMAGE"`. The obvious fix was `AD_IMAGE`, which is what Google's documentation and its own forum answers name for a Search image extension. **It does not work either.** Sent to a real account on v23, all four combinations are refused: `MARKETING_IMAGE` on a Search `CampaignAsset` → `FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE`; `MARKETING_IMAGE` on a Search `AdGroupAsset` → `UNSUPPORTED_FIELD_TYPE`; `AD_IMAGE` on a `CampaignAsset` → `UNSUPPORTED_FIELD_TYPE`; `AD_IMAGE` on an `AdGroupAsset` → `UNSUPPORTED_FIELD_TYPE`. Image extensions never moved to asset-based extensions, so there is no request the API takes. Every image field type is now refused at the CLI boundary with the interface named instead of another field type to try, and the **"recommend at least 3 image assets" advisory is removed** — it had been firing on every Search campaign since 0.191.0 and could not be satisfied by any draft, because the write it named has never once succeeded. Performance Max asset-group images are a different mechanism and are untouched (`asset-groups attach`). Four force-synced scaffold surfaces were saying the same wrong thing and now agree with the API: the `ads-google.md` tool doc, the `assets` command group description, the account-audit playbook's Dynamic Images row, and `staged-writes.md`. An audit should still raise a missing-images gap — it is a real SERP disadvantage — it just has to name the Google Ads interface as the fix rather than a command.
5851
+
5852
+ - **0.256.0**: a structured snippet value over 25 characters is refused before it is staged. Google caps each VALUE at 25 and answers a longer one with a bare `Too long.` at `structured_snippet_asset.values[i]` — no header, no value, nothing to act on — and it takes the asset's campaign link down with it as `RESOURCE_NOT_FOUND`. `assets create --values` and `assets update --values` now cap each entry; the per-asset 3-10 count check was already there, and only the per-value length was missing. Found on a live publish where four of five values were fine and "Bases de datos vectoriales" (26) was not, and confirmed fixed by re-publishing the same snippet at 25 characters against a real account, where it applied and linked cleanly.
5853
+
5818
5854
  ### Testing a pre-release in sandboxes
5819
5855
 
5820
5856
  ```bash
@@ -80,27 +80,10 @@
80
80
  const container = document.getElementById('captions');
81
81
  const tl = gsap.timeline({ paused: true });
82
82
 
83
- // A card closes where the SCRIPT closes, not on a word count. Slicing every
84
- // N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
85
- // preposition. The count stays as a ceiling (a long clause still has to be
86
- // split), and a linking word never ends a card: it belongs to what follows.
87
- const CLOSES = /[.,;:!?\u2026]$/;
88
- const CARRIES_ON = new Set([
89
- 'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
90
- 'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
91
- ]);
92
- const carriesOn = (w) => CARRIES_ON.has(String(w.word || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
93
-
94
83
  const groups = [];
95
- let current = [];
96
- for (const word of transcript) {
97
- current.push(word);
98
- const last = current[current.length - 1];
99
- const closes = CLOSES.test(String(last.word || '')) ||
100
- (current.length >= WORDS_PER_GROUP && !carriesOn(last));
101
- if (closes) { groups.push(current); current = []; }
84
+ for (let i = 0; i < transcript.length; i += WORDS_PER_GROUP) {
85
+ groups.push(transcript.slice(i, i + WORDS_PER_GROUP));
102
86
  }
103
- if (current.length > 0) groups.push(current);
104
87
 
105
88
  groups.forEach((group, gi) => {
106
89
  const el = document.createElement('div');
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debugLogSetting
3
- } from "./chunk-POYXIDOD.js";
3
+ } from "./chunk-DZUVUGEP.js";
4
4
 
5
5
  // src/debugLog.ts
6
6
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
@@ -150,4 +150,4 @@ export {
150
150
  readBodyForLog,
151
151
  installStreamTaps
152
152
  };
153
- //# sourceMappingURL=chunk-GXRUZ4LP.js.map
153
+ //# sourceMappingURL=chunk-2IB3IL4K.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-ECPNYXOA.js";
4
+ } from "./chunk-GU7IWEBC.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-POYXIDOD.js";
7
+ } from "./chunk-DZUVUGEP.js";
8
8
 
9
9
  // src/error-handler.ts
10
10
  var REQUEST_CONNECTION_PLATFORM = {
@@ -295,4 +295,4 @@ export {
295
295
  writeAdsOutput,
296
296
  resolveCustomerId
297
297
  };
298
- //# sourceMappingURL=chunk-TBEVU7XF.js.map
298
+ //# sourceMappingURL=chunk-6F52WYB7.js.map
@@ -1,25 +1,24 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-GXRUZ4LP.js";
4
+ } from "./chunk-2IB3IL4K.js";
5
5
  import {
6
6
  DIRECT_ROUTE,
7
7
  __commonJS,
8
8
  __toESM,
9
9
  captureProxyCredentials,
10
10
  childEnvWith,
11
- hasFrameVisionKey,
12
11
  isChallengeBody,
13
12
  isPrivateAddress,
14
13
  isProxyFailure,
15
14
  plannedRoutes,
16
15
  refuseNonPublicUrl,
17
16
  shouldEscalate
18
- } from "./chunk-POYXIDOD.js";
17
+ } from "./chunk-DZUVUGEP.js";
19
18
 
20
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js
19
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js
21
20
  var require_safe_stable_stringify = __commonJS({
22
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/index.js"(exports, module) {
21
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js"(exports, module) {
23
22
  "use strict";
24
23
  var { hasOwnProperty } = Object.prototype;
25
24
  var stringify = configure2();
@@ -1034,17 +1033,12 @@ function failureDetail(reason) {
1034
1033
  }
1035
1034
  return { message: describeFailureReason(reason) };
1036
1035
  }
1037
- function retryVerdict(retryable) {
1038
- if (retryable === true) return "transient \u2014 retry the run";
1039
- if (retryable === false) return "not retryable \u2014 retrying will not help, fix the input";
1040
- return "retryability unknown";
1041
- }
1042
1036
  function describeCause(c) {
1043
1037
  switch (c.kind) {
1044
1038
  case "local":
1045
1039
  return c.cause instanceof Error ? c.cause.message : String(c.cause);
1046
1040
  case "remote":
1047
- return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""} \u2014 ${retryVerdict(c.retryable)}`;
1041
+ return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""}`;
1048
1042
  case "timeout":
1049
1043
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
1050
1044
  case "network":
@@ -1098,7 +1092,7 @@ function resolveAdaptFormats(params) {
1098
1092
  return params.formats ?? [];
1099
1093
  }
1100
1094
 
1101
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/810146e81bae4e3a061fe487864f2fde80c4b03b886877dc0f1fffbc6480b67e/node_modules/safe-stable-stringify/esm/wrapper.js
1095
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/esm/wrapper.js
1102
1096
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1103
1097
  var configure = import__.default.configure;
1104
1098
  var wrapper_default = import__.default;
@@ -7259,24 +7253,6 @@ import { execFile as execFile3 } from "child_process";
7259
7253
  import { readFile as readFile9 } from "fs/promises";
7260
7254
  import path12 from "path";
7261
7255
  import { promisify as promisify3 } from "util";
7262
-
7263
- // src/engine/nodes/local/lib/caption-style.ts
7264
- var TERMINAL = /[.,;:!?…]$/;
7265
- function captionTextsFrom(html) {
7266
- const cards = html.match(/<div[^>]*class="[^"]*\bcaption\b[^"]*"[\s\S]*?<\/div>/g) ?? [];
7267
- return cards.map(
7268
- (card) => card.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim()
7269
- ).filter((text) => text.length > 0);
7270
- }
7271
- function fragmentedCaptions(texts) {
7272
- if (texts.length < 2) return null;
7273
- const fragments = texts.slice(0, -1).filter((t) => !TERMINAL.test(t));
7274
- if (fragments.length === 0) return null;
7275
- const example = fragments[0];
7276
- return `${fragments.length} of ${texts.length} caption cards end mid-clause, e.g. "${example}". Split captions where the script punctuates, not on a word count \u2014 a card that ends bare is a sentence cut in half by a timing decision, and it reads as one.`;
7277
- }
7278
-
7279
- // src/engine/nodes/local/lib/hyperframe-check.ts
7280
7256
  var execFileAsync = promisify3(execFile3);
7281
7257
  var NEVER_BLOCK = [
7282
7258
  /contrast/i,
@@ -7345,73 +7321,6 @@ function classifyCheckOutput(lintRaw, inspectRaw) {
7345
7321
  warnings: findings.filter((f) => f.severity === "warning")
7346
7322
  };
7347
7323
  }
7348
- var GATE_SECTIONS = ["lint", "runtime", "layout", "motion", "contrast"];
7349
- function buildCheckArgs(dir, samples) {
7350
- return ["hyperframes", "check", dir, "--json", "--samples", String(samples), "--at-transitions"];
7351
- }
7352
- var PROMOTE_TO_BLOCKING = /GSAP target [^ ]+ not found/i;
7353
- function toGateFinding(section, entry) {
7354
- const f = entry;
7355
- const code = typeof f.code === "string" ? f.code : "unknown";
7356
- const message2 = typeof f.message === "string" ? f.message : "";
7357
- const isError = f.severity === "error" || PROMOTE_TO_BLOCKING.test(message2);
7358
- return {
7359
- source: section,
7360
- code,
7361
- message: message2,
7362
- severity: isError && !isAdvisory(code, message2) ? "blocking" : "warning"
7363
- };
7364
- }
7365
- function classifyGateOutput(raw) {
7366
- const parsed = parseCheckJson(raw);
7367
- const findings = GATE_SECTIONS.flatMap((section) => {
7368
- const block = parsed?.[section];
7369
- const entries = Array.isArray(block?.findings) ? block.findings : [];
7370
- return entries.map((entry) => toGateFinding(section, entry));
7371
- });
7372
- return {
7373
- blocking: findings.filter((f) => f.severity === "blocking"),
7374
- warnings: findings.filter((f) => f.severity === "warning")
7375
- };
7376
- }
7377
- function summariseWarnings(warnings) {
7378
- const groups = /* @__PURE__ */ new Map();
7379
- for (const w of warnings) {
7380
- const key = `${w.source}/${w.code}`;
7381
- const seen = groups.get(key);
7382
- if (seen) seen.count += 1;
7383
- else groups.set(key, { count: 1, message: w.message });
7384
- }
7385
- return [...groups.entries()].sort((a, b) => b[1].count - a[1].count).map(([key, g]) => `${g.count}\xD7 [${key}] ${g.message}`);
7386
- }
7387
- var FINDING_ADVICE = {
7388
- sweep_static: "the timeline never advanced, so this renders as a STILL IMAGE while billing like a video \u2014 and every other green verdict in this run is unreliable. Check the composition registers a paused timeline on `window.__timelines[<composition-id>]` and that the tweens target elements that exist.",
7389
- console_error: "the composition threw in the browser, so whatever that script was driving did not happen.",
7390
- page_error: "a script failed to evaluate \u2014 animations and any JS-driven layout in this composition did not run.",
7391
- http_error: "an asset 404ed at render time; it will be missing from the frame, not substituted.",
7392
- console_warning: "if this names a GSAP target, the element it animates does not exist, so that tween is a no-op and whatever it was meant to reveal never appears. Fix the selector or add the element before rendering."
7393
- };
7394
- function explainGateFinding(finding) {
7395
- const advice = FINDING_ADVICE[finding.code];
7396
- return advice ? `${finding.message} \u2014 ${advice}` : finding.message;
7397
- }
7398
- function condenseFrameDescriptions(markdown) {
7399
- const lines = [];
7400
- let frame = null;
7401
- for (const line of markdown.split("\n")) {
7402
- const heading = line.match(/^## (.+)$/);
7403
- if (heading) {
7404
- frame = heading[1] ?? null;
7405
- continue;
7406
- }
7407
- const text = line.trim();
7408
- if (frame && text) {
7409
- lines.push(`${frame}: ${text}`);
7410
- frame = null;
7411
- }
7412
- }
7413
- return lines;
7414
- }
7415
7324
  function buildLintArgs(dir) {
7416
7325
  return ["hyperframes", "lint", dir, "--json"];
7417
7326
  }
@@ -7443,13 +7352,8 @@ async function lintStructuralIssues(dir, timeoutMs) {
7443
7352
  function buildInspectArgs(dir, samples) {
7444
7353
  return ["hyperframes", "inspect", dir, "--json", "--samples", String(samples)];
7445
7354
  }
7446
- function buildSnapshotArgs(dir, frames, vision) {
7447
- const base = ["hyperframes", "snapshot", dir, "--frames", String(frames)];
7448
- return vision ? [
7449
- ...base,
7450
- "--describe",
7451
- "Answer four things about this frame. (1) Is it empty or near-empty? Name every element actually visible. (2) Could what is shown physically happen \u2014 object sizes, human scale, and whether every object has its real-world form (a phone has ONE screen, on its front). Say so plainly if something is impossible. (3) Read out every number, currency symbol and word visible INSIDE the picture \u2014 phone screens, documents, signage \u2014 and say whether any of it contradicts the caption on this frame. (4) Is the caption legible against what is behind it?"
7452
- ] : [...base, "--describe", "false"];
7355
+ function buildSnapshotArgs(dir, frames) {
7356
+ return ["hyperframes", "snapshot", dir, "--frames", String(frames), "--describe", "false"];
7453
7357
  }
7454
7358
  function usesNestedCompositions(indexHtml) {
7455
7359
  const withoutComments = indexHtml.replace(/<!--[\s\S]*?-->/g, "");
@@ -7478,66 +7382,44 @@ async function runSnapshotSmoke(args, timeoutMs) {
7478
7382
  return { ok: false, unavailable: false, message: (err.stderr || err.message || "snapshot failed").slice(0, 800) };
7479
7383
  }
7480
7384
  }
7481
- async function runNestedCompositionSmoke(opts) {
7482
- const { dir, nodeId, ctx, timeoutMs, samples } = opts;
7483
- const indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7484
- if (!indexHtml || !usesNestedCompositions(indexHtml)) return;
7485
- const snap = await runSnapshotSmoke(
7486
- buildSnapshotArgs(dir, Math.min(samples, 3), hasFrameVisionKey()),
7487
- Math.max(timeoutMs, 12e4)
7488
- );
7489
- if (snap.unavailable) {
7490
- ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7491
- return;
7492
- }
7493
- if (!snap.ok) {
7494
- throw new Error(
7495
- `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7496
- ${snap.message}`
7497
- );
7498
- }
7499
- const described = await readFile9(path12.join(dir, "snapshots", "descriptions.md"), "utf-8").catch(() => "");
7500
- for (const line of condenseFrameDescriptions(described)) {
7501
- ctx.log(`${nodeId}: frame check \u2014 ${line}`);
7502
- }
7503
- ctx.log(`${nodeId}: nested-composition smoke test passed`);
7504
- }
7505
- async function runGate(dir, timeoutMs, samples) {
7506
- const checkRaw = await runOne(buildCheckArgs(dir, samples), timeoutMs);
7507
- if (checkRaw !== null) return classifyGateOutput(checkRaw);
7385
+ async function runHyperframesCheck(opts) {
7386
+ const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7508
7387
  const [lintRaw, inspectRaw] = await Promise.all([
7509
7388
  runOne(buildLintArgs(dir), timeoutMs),
7510
7389
  runOne(buildInspectArgs(dir, samples), timeoutMs)
7511
7390
  ]);
7512
- if (lintRaw === null && inspectRaw === null) return null;
7513
- return classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7514
- }
7515
- async function runHyperframesCheck(opts) {
7516
- const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7517
- const gate = await runGate(dir, timeoutMs, samples);
7518
- if (gate === null) {
7519
- ctx.log(`${nodeId}: hyperframes check/lint/inspect unavailable \u2014 skipping pre-render check`);
7391
+ if (lintRaw === null && inspectRaw === null) {
7392
+ ctx.log(`${nodeId}: hyperframes lint/inspect unavailable \u2014 skipping pre-render check`);
7520
7393
  return;
7521
7394
  }
7522
- const { blocking, warnings } = gate;
7523
- for (const line of summariseWarnings(warnings)) {
7524
- ctx.log(`${nodeId}: hyperframe check warning ${line}`);
7395
+ const { blocking, warnings } = classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7396
+ for (const w of warnings) {
7397
+ ctx.log(`${nodeId}: hyperframe check warning [${w.source}/${w.code}] ${w.message}`);
7525
7398
  }
7526
7399
  if (blocking.length > 0) {
7527
- const seen = /* @__PURE__ */ new Map();
7528
- for (const b of blocking) seen.set(`${b.source}/${b.code}/${b.message}`, b);
7529
- const detail = [...seen.values()].map((b) => `\u2022 [${b.source}/${b.code}] ${explainGateFinding(b)}`).join("\n");
7530
- throw new Error(
7531
- `${nodeId}: pre-render check failed (${blocking.length} blocking, ${seen.size} distinct)
7532
- ${detail}`
7533
- );
7400
+ const detail = blocking.map((b) => `\u2022 [${b.source}/${b.code}] ${b.message}`).join("\n");
7401
+ throw new Error(`${nodeId}: pre-render check failed (${blocking.length} blocking)
7402
+ ${detail}`);
7403
+ }
7404
+ let indexHtml = "";
7405
+ try {
7406
+ indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8");
7407
+ } catch {
7408
+ indexHtml = "";
7534
7409
  }
7535
- const composition = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7536
- const captionComplaint = fragmentedCaptions(captionTextsFrom(composition));
7537
- if (captionComplaint) {
7538
- ctx.log(`${nodeId}: caption style \u2014 ${captionComplaint}`);
7410
+ if (indexHtml && usesNestedCompositions(indexHtml)) {
7411
+ const snap = await runSnapshotSmoke(buildSnapshotArgs(dir, Math.min(samples, 3)), Math.max(timeoutMs, 12e4));
7412
+ if (snap.unavailable) {
7413
+ ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7414
+ } else if (!snap.ok) {
7415
+ throw new Error(
7416
+ `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7417
+ ${snap.message}`
7418
+ );
7419
+ } else {
7420
+ ctx.log(`${nodeId}: nested-composition smoke test passed`);
7421
+ }
7539
7422
  }
7540
- await runNestedCompositionSmoke({ dir, nodeId, ctx, timeoutMs, samples });
7541
7423
  ctx.log(`${nodeId}: pre-render check passed (${warnings.length} warning${warnings.length === 1 ? "" : "s"})`);
7542
7424
  }
7543
7425
 
@@ -9272,4 +9154,4 @@ export {
9272
9154
  defaultRegistry,
9273
9155
  createEngineFromEnv
9274
9156
  };
9275
- //# sourceMappingURL=chunk-ZK5S7JV2.js.map
9157
+ //# sourceMappingURL=chunk-77OP3PDC.js.map