@koda-sl/baker-cli 0.437.0-dev.ee2f8e252 → 0.438.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 +44 -42
- package/canvas/tiktok-captions-composition/index.html +6 -5
- package/canvas/tiktok-captions-composition/meta.json +0 -5
- package/dist/{chunk-FB3GTXQG.js → chunk-6PP3MBGZ.js} +791 -1067
- package/dist/chunk-6PP3MBGZ.js.map +1 -0
- package/dist/cli.js +5929 -7057
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +0 -10
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FB3GTXQG.js.map +0 -1
package/README.md
CHANGED
|
@@ -596,7 +596,7 @@ Write commands **never touch the Google Ads API at stage time**. Each command st
|
|
|
596
596
|
|
|
597
597
|
Reference not-yet-created resources with `g_temp_*` refs returned by earlier `create` commands (they map to Google's temporary negative-ID resource names inside one atomic batch).
|
|
598
598
|
|
|
599
|
-
**Staged ad copy is scanned for AI writing tells** (Google, Meta, and LinkedIn alike)
|
|
599
|
+
**Staged ad copy is scanned for AI writing tells** (Google, Meta, and LinkedIn alike). When a headline, description, or primary text carries a construction that reads as machine-written — a negation pivot, a fake-candid opener, a participle hung off a comma, a vague attribution — the stage response carries an `AI COPY TELL` entry in `hints[]`. A headline set where several headlines are one claim with a swapped verb is reported as `synonym-cycling`: an AI tell *and* a real auction defect. These are advisory and can never fail a staged write, deliberately — refusing an ad over a fallible prose heuristic costs more than a tell that ships. Thresholds are stricter than the landing critic's (one occurrence is enough, since a 30-character headline has no room for a cluster), and conventions that are legitimate in ad copy are excluded outright: Title Case headlines, staccato ad rhythm, emoji, and curly quotes.
|
|
600
600
|
|
|
601
601
|
```bash
|
|
602
602
|
# Build a Search campaign bottom-up, chaining temp refs
|
|
@@ -621,6 +621,18 @@ baker ads google campaigns create --customer-id 1234567890 --name "Brand — Sea
|
|
|
621
621
|
# New campaigns are declared as NOT containing EU political advertising (Google requires the
|
|
622
622
|
# declaration on every create); pass --eu-political-ads only when the campaign does contain it.
|
|
623
623
|
# On update, --eu-political-ads / --eu-political-ads=false corrects an existing declaration.
|
|
624
|
+
# Text guidelines (AI Max Search and Performance Max) stage on update only:
|
|
625
|
+
baker ads google campaigns update <campaign-id> --customer-id 1234567890 \
|
|
626
|
+
--term-exclusion "cheap" --messaging-restriction "Never promise a delivery date"
|
|
627
|
+
# --term-exclusion (repeatable, up to 25 × 30 chars) and --messaging-restriction (repeatable,
|
|
628
|
+
# up to 40 × 300 chars) each REPLACE the campaign's list; --clear-term-exclusions /
|
|
629
|
+
# --clear-messaging-restrictions empty one. A Search campaign needs AI Max with text
|
|
630
|
+
# customization on (in the same update or before), or staging is refused with the reason.
|
|
631
|
+
# AI Max for Search stages on create and update: --ai-max / --ai-max=false (off turns both
|
|
632
|
+
# features off too), --ai-max-text (text customization, needs AI Max) and
|
|
633
|
+
# --ai-max-url-expansion (final URL expansion, needs text customization). An omitted switch
|
|
634
|
+
# keeps the campaign's value; a combination Google refuses is refused at staging.
|
|
635
|
+
baker ads google campaigns update <campaign-id> --customer-id 1234567890 --ai-max --ai-max-text
|
|
624
636
|
baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
|
|
625
637
|
baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
|
|
626
638
|
# Shared negative keyword list → attach to the campaign
|
|
@@ -955,8 +967,7 @@ baker ads meta ads --account-id act_123 --all-statuses # widen to
|
|
|
955
967
|
baker ads meta ads --account-id act_123 --all-statuses --name-contains octubre --fields id,name,adset_id,creative_id
|
|
956
968
|
|
|
957
969
|
# "Who will see an ad in this campaign?" — a viewer per ad set: cold/warm/hot, what they
|
|
958
|
-
# already know (brand/offer/product), placements, and why — from targeting, audiences, objective
|
|
959
|
-
# and what each audience's name says (buyers, a lookalike), read in the advertiser's language
|
|
970
|
+
# already know (brand/offer/product), placements, and why — from targeting, audiences, objective
|
|
960
971
|
baker ads meta adsets --campaign-id 6123 --all-statuses --audience --output json
|
|
961
972
|
|
|
962
973
|
# "Tell me about ad 9988"
|
|
@@ -1172,7 +1183,8 @@ baker ads linkedin bid-pricing --account-id 503001492 \
|
|
|
1172
1183
|
**`analytics`** — the 3-axis workhorse:
|
|
1173
1184
|
- `--level account` (override: `campaign-group|campaign|creative`)
|
|
1174
1185
|
- `--intent baseline` field bundle. Intents: `baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity`. List with `--list-intents`.
|
|
1175
|
-
- `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
|
|
1186
|
+
- `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | dma | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
|
|
1187
|
+
- `--pivot dma` — US Nielsen Designated Market Areas, with region names resolved. LinkedIn's terms: any report built from it writes "DMA®" each time and carries "DMA® is a registered service mark of The Nielsen Company." — the `DMA_ATTRIBUTION_REQUIRED` warning repeats this on every call.
|
|
1176
1188
|
- `--granularity DAILY`. Auto-forced to `ALL` when pivoting on a `MEMBER_*` dim (LinkedIn rejects DAILY + demographic) — surfaced in stderr + `warnings`.
|
|
1177
1189
|
- `--last-days 7` unless you pass `--start`/`--end`.
|
|
1178
1190
|
- Sort: `costInUsd` descending. `--no-sort` to disable.
|
|
@@ -1278,6 +1290,8 @@ baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegmen
|
|
|
1278
1290
|
# Conversions with playbook-default windows
|
|
1279
1291
|
baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
|
|
1280
1292
|
--post-click-window 30 --view-window 7
|
|
1293
|
+
# Windows are 1|7|30|90 days; 180 and 365 only for LEAD, QUALIFIED_LEAD, MARKETING_QUALIFIED_LEAD,
|
|
1294
|
+
# SALES_QUALIFIED_LEAD, PURCHASE, ADD_TO_CART, SUBMIT_APPLICATION — refused at stage time otherwise
|
|
1281
1295
|
|
|
1282
1296
|
# Safe duplication — stage-time read of the source → staged DRAFT create ("Duplicate ad", never "Link to original")
|
|
1283
1297
|
baker ads linkedin campaigns duplicate 123456 --name "New variant"
|
|
@@ -1835,17 +1849,14 @@ Ads start from what customers said. Three commands carry the process; the agent
|
|
|
1835
1849
|
```bash
|
|
1836
1850
|
baker creatives brief [--adset <id>] [--account-id act_<id>] [--no-research] # one read: viewer, account bottleneck, what works, taste, customer quotes, ideas on file
|
|
1837
1851
|
baker creatives ideas [--full] # check and rank src/creatives/_ideas/*.md; build_next
|
|
1838
|
-
baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId>] [--spec <beats.json>] [--from-idea]
|
|
1839
|
-
baker creatives translate --slug <ad> --to es-MX,es-CO,pt-BR [--copy-file <json>] [--again] # the same ad in other languages, one design
|
|
1852
|
+
baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId>] [--spec <beats.json>] [--from-idea]
|
|
1840
1853
|
baker creatives learn [--days <n>] [--dry-run] # what each idea's ads did, written back onto the idea
|
|
1841
1854
|
```
|
|
1842
1855
|
|
|
1843
1856
|
- `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`; the market searches (the followed brands, and the library for what COMPANY.md's first line says the company sells, in a few words) run at once and get 12 seconds, so a slow ad library leaves them out rather than holding the brief. With few customer quotes on file it runs `research customers` itself alongside the reads (about 30 s) and returns what it wrote; `--no-research` skips that. With no ideas on file, its hint lists the values `awareness`, `angle` and `format` take. `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.
|
|
1844
|
-
- `ideas` is
|
|
1857
|
+
- `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.
|
|
1845
1858
|
- `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.
|
|
1846
1859
|
- `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, stopped and removed whole when another or none is kept), so the reference's describe pass no longer waits for them. Finding and describing a reference gets 50 seconds; past that the build is stopped and the static starts from the idea (`REFERENCE_SLOW` in the hint). A creative whose ad is already written is never built over (`ALREADY_MADE`): edit its `prompt.json`, or pass `--slug` for another ad of the same idea. 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.
|
|
1847
|
-
- `make --series <lead>` makes a static idea as another ad of a series: several messages in one style. Nothing is searched or rendered; the ad's `_definition.md` names its lead (`series: <lead>`), its `prompt.json` starts as the lead's ready version with the idea's message, and only its `copy` is written. Its canvas is written from the lead's on every run: the lead's finished picture with this ad's words, every other size the lead's own size with the same words. `baker canvas run --series <lead>` renders every ad of the series at once, one reviewed and corrected run each, skipping an ad already ready from the lead's current version.
|
|
1848
|
-
- `translate --slug <ad> --to <langs>` makes the ad in other languages as ads of the series it leads: one creative per language or country variant, `<ad>-<lang>` (the BCP-47 tag in lowercase: `-es-mx`), whose `_definition.md` says `series: <ad>` and `language: <lang>` (canonical case, `es-MX`, as the spec's market and the dashboard store it; tags are compared without case) and whose `prompt.json` is the ad's as last drawn with its words translated. Every language is translated in one text call (transcreated, each regional tag in that country's own variety and form of address, `es-MX` and `es-CO` two ads, each line keeping its role and about its length, brand names, prices and numbers kept, a customer's quote and the name on it never translated, and the ad's `message` translated with the lines; the spend is billed with the workspace's next run), or read from `--copy-file` (`{"<lang>": ["line 1", …]}`) with no model call. The ad may itself be an ad of a series (an angle ad), so angles × languages stay one design. A language already made is left as it is, so adding one later re-renders nothing; `--again` translates it again. Then `baker canvas run --series <ad>` renders them; a new version of the ad reaches every language on the next `--series` run.
|
|
1849
1860
|
|
|
1850
1861
|
### Competitive Intelligence (`baker research`)
|
|
1851
1862
|
|
|
@@ -3454,9 +3465,9 @@ Reading the scores: **`relevance`** (0–1) = match of the creative to your quer
|
|
|
3454
3465
|
|
|
3455
3466
|
### `baker winning-ads hooks|mechanisms|formats`
|
|
3456
3467
|
|
|
3457
|
-
Read the
|
|
3458
|
-
mechanism / static-format patterns that
|
|
3459
|
-
|
|
3468
|
+
Read the pattern rollup of the ad-dna corpus → `GET /api/ad-library/seed-catalog` — the hook /
|
|
3469
|
+
mechanism / static-format patterns that win **inside brands**, among the creatives of the segment you pass
|
|
3470
|
+
(`--platform`, `--awareness`, `--industry`; an unset one means any), keyed on the closed seeds/enums the
|
|
3460
3471
|
extractor emits (no clustering). Ground a creative in a **proven** pattern for the exact segment instead of
|
|
3461
3472
|
guessing. The rollup is recomputed daily.
|
|
3462
3473
|
|
|
@@ -3478,8 +3489,10 @@ baker winning-ads formats --awareness solution_aware --output md
|
|
|
3478
3489
|
| `--limit <n>` | Max keys 1–100 (default 20) |
|
|
3479
3490
|
| `--full` | Include the per-key segment breakdown |
|
|
3480
3491
|
|
|
3481
|
-
Each key returns `count`, `mean_winner_score
|
|
3482
|
-
|
|
3492
|
+
Each key returns `lift`, `brands_for`, `brands_against`, `count`, `mean_winner_score` and `winner_share`. The
|
|
3493
|
+
ranking is `lift`: how much more often a brand's creatives using the pattern kept running 60 days than that
|
|
3494
|
+
brand's others, one vote per brand, 0 until five brands have a verdict. `brands_for` / `brands_against` count
|
|
3495
|
+
the brands it won and lost inside. A common pattern is not a winning one: `count` is how often it is used.
|
|
3483
3496
|
|
|
3484
3497
|
### `baker winning-ads fit <adId>`
|
|
3485
3498
|
|
|
@@ -4393,18 +4406,8 @@ because a video model garbles a wordmark every time:
|
|
|
4393
4406
|
order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
|
|
4394
4407
|
(vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
|
|
4395
4408
|
the ad needs something other than the client's default.
|
|
4396
|
-
- **The spoken language is read off the script** when
|
|
4397
|
-
|
|
4398
|
-
any language, so a Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
4399
|
-
The same read, in any language and in a line that mixes two, finds a line where the speaker
|
|
4400
|
-
introduces themselves with nobody on camera, a shot brief that asks for readable text (both
|
|
4401
|
-
refused), a cast who works for the business, each voice description's gender, and the words
|
|
4402
|
-
a caption card may not end on. It is a fraction of a cent, kept until the spec changes and
|
|
4403
|
-
billed with the next run; when it cannot be read, nothing is refused and the hints say so.
|
|
4404
|
-
- **`market` is where the ad is set**, stated on every frame. Omitted, it is the country of a
|
|
4405
|
-
regional language (`es-MX` → Mexico), the voice's or the translation's. A language alone
|
|
4406
|
-
names no country (Spanish is not Spain): with neither, the frames name no place and a hint
|
|
4407
|
-
asks for one.
|
|
4409
|
+
- **The spoken language is read off the script** when the spec does not declare it, so a
|
|
4410
|
+
Spanish ad is cast from Spanish voices without anyone having to say so.
|
|
4408
4411
|
- **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
|
|
4409
4412
|
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
4410
4413
|
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
@@ -5895,9 +5898,8 @@ $ baker canvas critique my-static-ad.json
|
|
|
5895
5898
|
|
|
5896
5899
|
Would the person a static ad reaches understand it? A model plays that viewer — what they already know and
|
|
5897
5900
|
where they scroll — and says, from `prompt.json`'s copy and scene alone, what is offered, to whom and what to do
|
|
5898
|
-
next; the
|
|
5899
|
-
|
|
5900
|
-
when the viewer can only guess what is offered, is sure of something else, or, for a viewer who does not know the brand, when the words say they already did something with it (a
|
|
5901
|
+
next; the answer is compared with `prompt.json`'s `message`. The ad fails when the viewer can only guess what is
|
|
5902
|
+
offered, or, for a viewer who does not know the brand, when the words say they already did something with it (a
|
|
5901
5903
|
cart, a trial, a donation, a registration); who it is for is reported but never fails an ad alone. The viewer is the `viewer` recorded in
|
|
5902
5904
|
`_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
|
|
5903
5905
|
the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
|
|
@@ -5933,7 +5935,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5933
5935
|
| `--fix "<what to change>"` | none | Static ad only: correct a finished version with one render that changes only what you say; every size is made again from it (see below). The same fix after it left its defect in place is refused (`no_progress`). |
|
|
5934
5936
|
| `--use-variant <sha256>` | none | Static ad only: make every size again from another of a version's variants, the one the person chose; the only requests re-lay it out for each other size (see below). |
|
|
5935
5937
|
| `--from <run_id>` | the last render | With `--fix` or `--use-variant`: the version to start from. |
|
|
5936
|
-
| `--autofix` / `--no-autofix` | on | Static ad only: when the review finds a blocking defect, make up to
|
|
5938
|
+
| `--autofix` / `--no-autofix` | on | Static ad only: when the review finds a blocking defect, make up to 2 more renders (`AUTO_FIX_ROUNDS`) that correct it before reporting (see below). `--no-autofix` reports after the first render. |
|
|
5937
5939
|
| `--skip-viewer-check` | off | Static ad only: render without the viewer check (`baker canvas viewer-check`), when the person heard what the viewer would miss and wants the copy as it is. |
|
|
5938
5940
|
| `--remote-cache <on\|off>` | `on` (or `BAKER_CANVAS_REMOTE_CACHE`) | Operator only. Company-scoped remote cache + durable asset persistence. |
|
|
5939
5941
|
| `--no-record` | records | Skip posting the durable run-history record (and its live progress). |
|
|
@@ -5948,7 +5950,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
|
|
|
5948
5950
|
|
|
5949
5951
|
**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`).
|
|
5950
5952
|
|
|
5951
|
-
**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
|
|
5953
|
+
**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, or that the placing found inside its area while the review still blocks it, is laid out again instead, never placed the same way twice; a button is placed by its shape, its words grown by 0.7 of their height on every side, as the review judges it); 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.
|
|
5952
5954
|
|
|
5953
5955
|
**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.
|
|
5954
5956
|
|
|
@@ -6061,7 +6063,7 @@ The two scaffold passes are billed (the full `video_deconstruct` is the heavy on
|
|
|
6061
6063
|
|
|
6062
6064
|
Turn a source/inspiration image into a **runnable, self-validated static-ad canvas** — the static counterpart of `scaffold-video`. It runs **billed Gemini passes** up front:
|
|
6063
6065
|
|
|
6064
|
-
1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels;
|
|
6066
|
+
1. **`image_describe`** (`~google/gemini-pro-latest`) — reverse-engineers the image (layout by what fills each region, copy verbatim, typefaces, capture in words, the `mechanism` and the `audience` it is built for) and writes it next to the canvas as **`prompt.json`**, an **ad spec** (`"schema": "baker-static-ad/3"`; `/2` still reads) you rewrite into the ad you want: `viewer` (who sees it and what they already know), `message` (the one thing it tells them, never printed), `copy` (each line with `lettering`: `in_scene`, written on something in the picture (`on`, `look`), or `designed`, set in the brand's face), `scene` (`description`, `look`: photo | designed | illustration, `capture` in words, `mood`, `layout`), `brand`, `colors` (each tied to an object, from the brand palette), `references` (one picture each, labelled by role: `PERSON_1`, `ANIMAL_1`, `PRODUCT_1`, `BADGE_1`, `PROP_1`), `logo` (and `logo_position`), `third_party_brands` (other brands the offer is about, a car brand on a car loan: `name`, `what`, and `logo` naming the `BADGE_n` reference of its logo; their logos may appear), an optional `direction`, the `mechanism` to rebuild for our brand, and in `/3`: `viewer.market` (`country`, `language`, `currency`), `brand_presence` (`standard` | `native`: a native disguise carries the logo as its sender and no big button), `recognition` (the kind of product a stranger must take it for at a glance, never the format), `native` (the ad is a screen people know, drawn exactly from HTML at every size by the free `static_native` node instead of painted: `x_post` (a post or a thread, with `reply_to`), `whatsapp`, `imessage`, `trustpilot`, `google_reviews`, `notes`, `slack`; on a 9:16 the app is drawn full screen as a phone shows it: status bar and app header pinned at the top, the reply bar and tab bar (or a keyboard under a chat or note) pinned at the bottom, the posts or messages as large as the clear band holds, and the frame's headline, caption and logo a card in the brand's colours under the app's header; a wall of reviews (`trustpilot`, `google_reviews`) takes up to 16 real reviews, the first two (`FEATURED_REVIEWS`) the ad's copy set whole in the safe area and the rest filling the whole frame behind them as large, sharp cards, turned and cut by its edges, because a wall wins on how many there are; a review longer than 160 characters is shown cut at a word with "…", and that cut text is the copy the review reads back; a chat (`whatsapp`, `imessage`) on a 9:16 never shrinks below about 62% of the phone scale: its earliest messages scroll up under the header, as the app does, the node's optional `scrolled` output lists the copy lines scrolled off that size, and the run review expects those lines nowhere on it; a logo file whose drawing runs off its own edge is set aside for a whole one, and a square mark where a wordmark goes is drawn larger; the run review never flags the app's own interface words (status bar, keyboard) as under Reels; the image model renders only the screen's `photos`, each a plain phone photo of ordinary people of the market; adding the block switches an existing ad on its next run, `--fix` does not apply, an invented brand handle or a rating the Knowledge does not hold (either decimal mark) is refused as `native_screen_invalid`, and a photo of ours goes in as its `image`: a native photo takes no `avatar`, since nothing would cast it), a role on each third-party brand (`partner` | `proof` | `category_signal`), a reference's `image` (wired as a picture of ours) and a person's `avatar` (the company avatar cast), and `concept` (`why_it_wins` in five layers, `load_bearing` elements with `have`/`adapt`/`missing` and `ours`, `verdict` `use`|`adapt`|`reject`, `transfer_note`; seeded from `baker winning-ads fit`). Every render keeps the load-bearing elements; `validate`/`run` refuse a `reject` or a `missing` element with nothing named (`concept_does_not_fit`) and a rating, or a promised price, share, multiple or time, the Knowledge does not hold with that unit, a number spelled in words included, or a superlative it does not state in the same words (`proof_without_source`); a line citing a review platform gets its real badge wired. **You never write a model prompt**: every render prompt is compiled from this file for the model it runs on (short labelled sections for gpt-image, sentences with reasons for Gemini; each line quoted once; pictures numbered with one role each; colours from the brand; realism as capture cues; the safe area from its one owner; constraints last), on every `validate` and `run`, and shown on its node. A `prompt.json` written before the ad spec (the describe blueprint) is read through an adapter and keeps working. The source's own read (`source`, and the library's `reference`) stays as bookkeeping and is never sent to a model.
|
|
6065
6067
|
2. **element selection** (`~google/gemini-flash-latest`) — picks the **main, identity-critical** elements (the brand logo, a showcased product, a trust badge) **plus any foreground/hero person or animal** — the emotional focal point — even a generic one, because a free-generated face/muzzle reads as AI and grows artifacts; the emotional hero always gets a real-reference slot. When the advertiser's logo appears in **more than one lockup** (a square/icon **mark** and a horizontal **wordmark**), each is emitted as its **own** element (e.g. `LOGO_MARK`, `LOGO_WORDMARK`). The describe pass also records the ad's **typography** (each typeface's classification, a best-guess family, and its weight/case), which seeds the spec's `brand.typeface`. Background extras are dropped. Each element becomes a `references` entry in the spec, named by its role and naming the canvas slot it fills, so the file says which slot grounds which subject.
|
|
6066
6068
|
|
|
6067
6069
|
It then scaffolds a canvas on which **the image model designs the finished ad**, holding only what `prompt.json` holds; no element is mandatory:
|
|
@@ -6078,7 +6080,7 @@ The canvas is validated before it is written. stdout returns `{ ok, canvas_path,
|
|
|
6078
6080
|
|
|
6079
6081
|
**Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
|
|
6080
6082
|
|
|
6081
|
-
**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; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene, except a price (a currency sign beside a figure) the copy does not hold, blocking at any size and blurred by the correction, the document it is on kept; every render is asked for no readable prices on a bill, quote, receipt, screen or price tag beyond the copy; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"
|
|
6083
|
+
**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; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene, except a price (a currency sign beside a figure) the copy does not hold, blocking at any size and blurred by the correction, the document it is on kept; every render is asked for no readable prices on a bill, quote, receipt, screen or price tag beyond the copy; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, 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-candidate 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, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections) and about 8 minutes in all: a correction round that would take it past that is not started (`[autofix] not started`), the version shown as it stands, and no render starts once it is spent or once the ad is `ready` or `ready_with_notes`: the next is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
|
|
6082
6084
|
|
|
6083
6085
|
The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
|
|
6084
6086
|
|
|
@@ -6605,7 +6607,7 @@ import {
|
|
|
6605
6607
|
|
|
6606
6608
|
## Creatives
|
|
6607
6609
|
|
|
6608
|
-
|
|
6610
|
+
Publish an approved canvas render as a first-class Baker creative: a static image (PNG, JPG, WebP) or a finished video (MP4, MOV, WebM). An image uploads to the Baker image library (tagged `creative`); a video uploads to the Baker video library and is saved once it plays (bounded at 3 minutes). A creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
|
|
6609
6611
|
|
|
6610
6612
|
```bash
|
|
6611
6613
|
baker creatives publish --title "Spring Offer 4x5" \
|
|
@@ -6623,8 +6625,9 @@ baker creatives publish ./canvas/<run_id>/final.mp4 --title "Spring Offer Reel"
|
|
|
6623
6625
|
| `<file>` | The image or video. For an image it is optional with `--run-id`: the run's own saved output (from its run record, sha-verified) is used instead, so a workspace that never had the run's folder can publish it. |
|
|
6624
6626
|
| `--title <text>` | Required. Human title for the creative. |
|
|
6625
6627
|
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. With `--run-id` an image publish also sets `status: published` in the creative's `_definition.md` (`definitionStatus` in the output). |
|
|
6626
|
-
| `--run-id <r_…>` | Pins the
|
|
6628
|
+
| `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
|
|
6627
6629
|
| `--output <label\|slot\|n>` | With `--run-id` and no file: which of the run's images to save when it made several (a label such as `4:5`, a slot, or a 1-based number). |
|
|
6630
|
+
| `--approved` | The person's own message asked to save it. Needed only for an image run rendered in the current chat turn (`BAKER_TURN_ID`), which they have not seen yet; without it that save is refused. |
|
|
6628
6631
|
| `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
|
|
6629
6632
|
| `--context <text>` | Optional describe context for the uploaded image or video. |
|
|
6630
6633
|
|
|
@@ -6686,7 +6689,7 @@ After a rejection, `hints[]` restates what to change, starting from that version
|
|
|
6686
6689
|
|
|
6687
6690
|
### `baker creatives select`
|
|
6688
6691
|
|
|
6689
|
-
|
|
6692
|
+
Choose the version of a creative to keep, or only some of its options, the way choosing it in Baker does: the creative shows that version as the chosen one, and the choice records the chat sender's approval (`source: "selection"`). Naming options replaces the earlier pick. It does not add the image to the library; that is `baker creatives publish`, which, given `--run-id`, also counts as choosing that version.
|
|
6690
6693
|
|
|
6691
6694
|
```bash
|
|
6692
6695
|
baker creatives select --slug spring-offer # the version it shows now
|
|
@@ -6768,7 +6771,7 @@ baker landing export list # the last 1
|
|
|
6768
6771
|
|
|
6769
6772
|
### `baker landing critique`
|
|
6770
6773
|
|
|
6771
|
-
|
|
6774
|
+
Deterministic design-quality critic for landing pages — an offline, no-LLM detector for the well-known "AI slop" tells (gradient text, overused fonts like Inter, side-tab colored borders, cream/parchment grounds, purple-on-heading palettes, buzzword copy, broken images, and more). It scores a landing's `.astro` source and returns findings tiered **block / warn / advisory**.
|
|
6772
6775
|
|
|
6773
6776
|
```bash
|
|
6774
6777
|
baker landing critique spring-offer # score src/pages/spring-offer/
|
|
@@ -6778,15 +6781,14 @@ baker landing critique spring-offer summer-offer claude # score three landings
|
|
|
6778
6781
|
|
|
6779
6782
|
- **Takes several slugs at once.** `data.landings` is always an array with one entry per slug (`{ slug, overall, counts, dimensions, findings }`). For a single slug those fields are also spread at the top of `data`, so existing one-landing readers are unaffected. A wrong slug anywhere in the batch fails the whole call before any landing is scored.
|
|
6780
6783
|
- **Advisory by design — findings never fail it.** A finding is guidance, not a gate; the command errors (exit 2) only on a wrong slug (`NOT_FOUND` / `INVALID_SLUG`, with `error.fix.availableSlugs`).
|
|
6781
|
-
- **The
|
|
6782
|
-
- **The brand is the allowlist.** A font, hue, or ground the client's brand commits is a decision, not a tell — matched values downgrade to advisory `brandDrift` so the critic never fights a real brand. The allowlist reads `src/brand/tokens.json` (the canonical machine-readable brand token file) when present; workspaces that predate it fall back to scanning the `@theme` block in `src/styles/global.css` (hex or oklch) plus the faces and colours `src/brand/BRAND.md` says the brand uses, read in any language by the brand reader — a face or colour BRAND.md bans ("nunca uses Inter") is never allowlisted.
|
|
6784
|
+
- **The brand is the allowlist.** A font, hue, or ground the client's brand commits is a decision, not a tell — matched values downgrade to advisory `brandDrift` so the critic never fights a real brand. The allowlist reads `src/brand/tokens.json` (the canonical machine-readable brand token file) when present; workspaces that predate it fall back to scanning the `@theme` block in `src/styles/global.css` (hex or oklch) plus `src/brand/BRAND.md` prose.
|
|
6783
6785
|
- **Records a freshness snapshot** to `.cache/landing-critique/<slug>.json` (the source hash it scored). The publish quality gate reads it to enforce that every changed landing was critiqued and ships no block-tier tell. Re-run after edits so the snapshot stays fresh.
|
|
6784
6786
|
|
|
6785
|
-
- **AI writing tells are scored in the `copy` family, in
|
|
6787
|
+
- **AI writing tells are scored in the `copy` family, in English and Spanish.** Beyond buzzwords and em-dash density, the critic detects the constructions models over-produce: the negation pivot ("it's not just X, it's Y" / "no se trata solo de X, sino de Y"), fake-candid openers ("Honestly?", "La verdad es que"), participles hung off a comma (", ensuring…", ", permitiendo…"), copula avoidance ("serves as", "cuenta con"), vague attributions, signposting, filler, hollow closers, false ranges, staccato runs, Title Case headings, and chatbot framing that leaked into the page. Most need a **cluster** (≥2 on the page) before they fire — the source guidance is explicit that isolated tells prove nothing — with two deliberate exceptions: chatbot framing fires at one, and curly quotes are not detected at all, since every editor inserts them automatically. Nothing in this family is block-tier, so it never fails the publish gate. Pattern set adapted from Wikipedia's "Signs of AI writing" (WikiProject AI Cleanup).
|
|
6786
6788
|
|
|
6787
|
-
- **Agent washing is a copy tell.** A page
|
|
6789
|
+
- **Agent washing is a copy tell.** A page that claims autonomy ("fully autonomous", "while you sleep", "no human intervention") and never says anywhere what the buyer approves, overrides, or audits scores an `agent-washing` warn. One signal about oversight of the agent's actions — approval, review, override, undo/rollback, audit log, escalation — anywhere on the page clears it; the rule is page-scope precisely because the hero can defer the trust story to a section below it. Generic privacy boilerplate ("GDPR", "encrypted", "your data") deliberately does **not** clear it, since it appears in most footers and says nothing about who is in charge of the agent.
|
|
6788
6790
|
|
|
6789
|
-
- **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a sentence outside a `<table>`
|
|
6791
|
+
- **Positioning integrity is scored too, and it is the one copy rule that blocks on a single sentence.** `competitor-concession` fires when the page ranks somebody else above the client, or says the client does not lead at what the page sells ("su constructor es mejor que el nuestro", "we're not the best"). Every other prose rule waits for a cluster because one AI tell proves nothing; this one is the whole defect on its own — the page is paid traffic, and it is arguing the visitor should leave. Two shapes that read identically are exempt by design: a concession about **price**, in either direction ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and a superlative flipped by a negation ("ningún equipo trabaja más rápido que el nuestro"), which is the strongest claim *for* the client. Its warn-tier sibling `competitor-named-in-comparison` fires when a name from `src/content/competitors/` appears in a comparative sentence outside a `<table>` — a comparison table is a section the landing skill teaches, but naming a rival anywhere else on paid traffic is the client's call, not the build's. Spanish, Portuguese and English.
|
|
6790
6792
|
|
|
6791
6793
|
- **Comments are not markup, and no rule reads one.** Astro expression comments (`{/* … */}`) and HTML comments (`<!-- … -->`) are blanked before detection, offsets preserved so `file:line` stays exact. A shared scaffold component explaining its `preconnect` in prose that spelled `<img>` twice was read as two broken images — block-tier — so every landing rendering a video failed the publish gate on a file the client cannot edit. Comment prose is also where a component's reasoning lives, em-dashes and buzzwords included, so reading it inflated the count-threshold tells too.
|
|
6792
6794
|
|
|
@@ -87,12 +87,13 @@
|
|
|
87
87
|
// A card closes where the SCRIPT closes, not on a word count. Slicing every
|
|
88
88
|
// N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
|
|
89
89
|
// preposition. The count stays as a ceiling (a long clause still has to be
|
|
90
|
-
// split), and a word
|
|
91
|
-
// those are is decided per script when the canvas is scaffolded, in the
|
|
92
|
-
// script's own language (`carries_on`); empty, the ceiling alone closes a card.
|
|
90
|
+
// split), and a linking word never ends a card: it belongs to what follows.
|
|
93
91
|
const CLOSES = /[.,;:!?\u2026]$/;
|
|
94
|
-
const CARRIES_ON = new Set(
|
|
95
|
-
|
|
92
|
+
const CARRIES_ON = new Set([
|
|
93
|
+
'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
|
|
94
|
+
'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
|
|
95
|
+
]);
|
|
96
|
+
const carriesOn = (w) => CARRIES_ON.has(String(w.text || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
|
|
96
97
|
|
|
97
98
|
const groups = [];
|
|
98
99
|
let current = [];
|
|
@@ -28,11 +28,6 @@
|
|
|
28
28
|
"default": 3,
|
|
29
29
|
"description": "How many words to show at once."
|
|
30
30
|
},
|
|
31
|
-
"carries_on": {
|
|
32
|
-
"kind": "json",
|
|
33
|
-
"default": [],
|
|
34
|
-
"description": "Words a card may not end on, lowercase: they lean on the word after them. Decided per script when the canvas is scaffolded."
|
|
35
|
-
},
|
|
36
31
|
"font_size": {
|
|
37
32
|
"kind": "integer",
|
|
38
33
|
"min": 24,
|