@koda-sl/baker-cli 0.273.0 → 0.274.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 +6 -0
- package/dist/cli.js +250 -19
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5740,6 +5740,10 @@ baker landing critique spring-offer summer-offer claude # score three landings
|
|
|
5740
5740
|
|
|
5741
5741
|
- **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.
|
|
5742
5742
|
|
|
5743
|
+
- **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.
|
|
5744
|
+
|
|
5745
|
+
- **Copy held in component frontmatter is read like markup copy.** Astro landings keep FAQ pairs, testimonial quotes, feature cards and pricing rows as a `const items = [{ q, a }]` array above the `---` fence and render them in a `.map()`, so a critic that blanked the whole block was blind to a large share of every page's actual words. Prose string literals in that block now feed every copy rule; imports, class strings, URLs and config do not, and the bar is deliberately high in that direction — a Tailwind class list in front of the copy rules is worse than one missed tell.
|
|
5746
|
+
|
|
5743
5747
|
Output is the standard envelope `{ ok, data, hints }` with `data = { advisory, slug, overall, counts, dimensions, findings }`. `dimensions` scores seven design families (typography, color, borders_depth, motion, spacing, copy, integrity) 0–1 (higher is better); `counts` is the block/warn/advisory tally.
|
|
5744
5748
|
|
|
5745
5749
|
## Brand Fonts (`baker brand fonts`)
|
|
@@ -5875,6 +5879,8 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
|
|
|
5875
5879
|
|
|
5876
5880
|
- **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.
|
|
5877
5881
|
|
|
5882
|
+
- **0.274.0**: `baker landing critique` gains a `positioning integrity` pair in the `integrity` family, and starts reading the copy Astro components keep in their frontmatter. `competitor-concession` is **block-tier and fires at a single sentence** — unlike every other prose rule, which needs a cluster — because a page that ranks a competitor above the client, or says the client does not lead at what it sells, spends the client's ad budget arguing the visitor should leave. A concession about **price** in either direction never fires it ("we are not the cheapest / the most expensive, and here is why" is premium positioning), and neither does 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>` — naming a rival on paid traffic is the client's call. Both read Spanish, Portuguese and English. Alongside them, prose string literals in a component's `---` block are now scored like markup copy (imports, class strings, URLs and config still are not), so an FAQ held as `const faqs = [{ q, a }]` — the standard Astro shape — is no longer invisible to the critic and to `BRAND.md` copy rules. `CRITIC_VERSION` bumps to `3`.
|
|
5883
|
+
|
|
5878
5884
|
- **0.273.0**: **an ad's name can no longer take a whole publish down.** `Ad.name` is IMMUTABLE, and `--ad-name` is shared by `ads create` and `ads update` (every content flag is), so an update restating the ad's name put `name` in the field mask and Google refused the operation — and, being one op inside an atomic batch, everything staged with it. The name is now dropped from an update's mutate, and an update that would actually RENAME the ad is refused at stage time instead, so a rename is answered rather than silently ignored. Two related corrections: `--ad-name` is required on `demandGenVideo` **only** — a multi-asset `demandGen` ad published with no name at all and Google created it, so the docs that called it a Demand-Gen-family requirement were wrong — and a content flag on `--format call` is now refused like every other format's, instead of being dropped because a call ad reads its content from `--file` alone. The Demand Gen video ad's live videos and logos are also read into the review card now, so an update that replaces them shows what it is replacing.
|
|
5879
5885
|
|
|
5880
5886
|
- **0.272.0**: what one live publish of a Demand Gen video ad found, all four fixed and each of them a whole-batch failure (a campaign, its ad group, its ad and its assets are one atomic mutate). `--type DEMAND_GEN_AD` and `--type VIDEO_OUTSTREAM` are **gone from `ad-groups create`** — v23's ad group types have never had either, and Google answers with a request-level `Invalid value` that fails every sibling operation. `--type` is now **optional and best omitted**: Google derives an ad group's type from its campaign's channel, and on a Demand Gen campaign that is the only way to create one (its `ad_group.type` comes back empty). `--bidding-strategy MANUAL_CPC` on a **Demand Gen** campaign is refused before staging — Google rejects it outright (`OPERATION_NOT_PERMITTED_FOR_CONTEXT`), and the Demand Gen playbook shipped in every sandbox was teaching it; `MAXIMIZE_CONVERSIONS` and `TARGET_CPC` work. And a new Demand Gen video ad now needs `--ad-name` (Google's "Ad name", required on Demand Gen ads and no other format) and at least one `--logo-image-assets` ref alongside its video — Google only says so at publish, once the assets in the same batch have already been created.
|
package/dist/cli.js
CHANGED
|
@@ -43290,7 +43290,7 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
43290
43290
|
import { defineCommand as defineCommand168 } from "citty";
|
|
43291
43291
|
|
|
43292
43292
|
// src/commands/landing/critique.ts
|
|
43293
|
-
import { readdir as readdir9, stat as stat6 } from "fs/promises";
|
|
43293
|
+
import { readdir as readdir9, readFile as readFile22, stat as stat6 } from "fs/promises";
|
|
43294
43294
|
import path28 from "path";
|
|
43295
43295
|
import { defineCommand as defineCommand158 } from "citty";
|
|
43296
43296
|
|
|
@@ -43420,6 +43420,29 @@ var COPY_TELL_GATES = {
|
|
|
43420
43420
|
"inline-header-list": { minCount: 3, severity: "advisory" },
|
|
43421
43421
|
emoji: { minCount: 3, severity: "advisory" }
|
|
43422
43422
|
};
|
|
43423
|
+
var CONCESSION_PATTERNS = [
|
|
43424
|
+
// Cedes superiority to somebody else.
|
|
43425
|
+
/\b(?:mejor|peor)(?:es)? que (?:el |la |lo |los |las )?nuestr[oa]s?\b/g,
|
|
43426
|
+
/\bmas [a-z]+ que (?:el |la |lo |los |las )?nuestr[oa]s?\b/g,
|
|
43427
|
+
/\b(?:melhor|pior)(?:es)? que (?:o |a |os |as )?noss[oa]s?\b/g,
|
|
43428
|
+
/\bmais [a-z]+ que (?:o |a |os |as )?noss[oa]s?\b/g,
|
|
43429
|
+
/\bbetter than (?:ours|our own)\b/g,
|
|
43430
|
+
/\b(?:ellos|ellas) lo hacen mejor\b/g,
|
|
43431
|
+
/\beles fazem melhor\b/g,
|
|
43432
|
+
/\bthey do it better\b/g,
|
|
43433
|
+
/\bnos (?:ganan|superan|adelantan) en\b/g,
|
|
43434
|
+
/\bnos (?:ganham|superam) em\b/g,
|
|
43435
|
+
// Denies the client leads.
|
|
43436
|
+
/\b(?:hoy |actualmente )?no somos nosotros\b/g,
|
|
43437
|
+
/\b(?:hoje |atualmente )?nao somos nos\b/g,
|
|
43438
|
+
/\bno somos (?:el|la|los|las) (?:mejor|mas) [a-z]+/g,
|
|
43439
|
+
/\bno somos (?:el|la|los|las) (?:mejores|mejor)\b/g,
|
|
43440
|
+
/\bnao somos (?:o|a|os|as) (?:melhor|melhores|mais) ?[a-z]*/g,
|
|
43441
|
+
/\bwe(?:'re| are) not the (?:best|most|leading|strongest)\b/g,
|
|
43442
|
+
/\b(?:it|that)(?:'s| is) not us\b/g
|
|
43443
|
+
];
|
|
43444
|
+
var PRICE_CONCESSION_CONTEXT = /barat|econom|precio|preco|cheap|price|low ?cost|coste|custo|\bcar[oa]s?\b|\bexpensive\b/;
|
|
43445
|
+
var CONCESSION_NEGATION_CONTEXT = /\b(?:ningun[oa]?s?|nadie|nada|no hay|no existe|nenhum[oa]?s?|ninguem|nao ha|nao existe|nothing|nobody|no one|none|there is no)\b/;
|
|
43423
43446
|
var AUTONOMY_CLAIMS = [
|
|
43424
43447
|
"fully autonomous",
|
|
43425
43448
|
"completely autonomous",
|
|
@@ -43499,6 +43522,16 @@ var RULE_META = {
|
|
|
43499
43522
|
severity: "block",
|
|
43500
43523
|
note: "An <img> with an empty or placeholder src ships a broken image. Source a real asset via `baker images`."
|
|
43501
43524
|
},
|
|
43525
|
+
"competitor-concession": {
|
|
43526
|
+
family: "integrity",
|
|
43527
|
+
severity: "block",
|
|
43528
|
+
note: "This page ranks somebody else above the client, or says the client does not lead. A paid landing answers an objection by changing the axis, never by conceding it. Rewrite the claim around what this product does that the alternative structurally cannot (`references/sections/faq.md`)."
|
|
43529
|
+
},
|
|
43530
|
+
"competitor-named-in-comparison": {
|
|
43531
|
+
family: "integrity",
|
|
43532
|
+
severity: "warn",
|
|
43533
|
+
note: "A competitor from the vault is named in a comparative sentence outside a comparison table. Naming a rival on paid traffic sells their brand and is the client's decision, not yours \u2014 cite the capability, not the company."
|
|
43534
|
+
},
|
|
43502
43535
|
"side-tab": {
|
|
43503
43536
|
family: "borders_depth",
|
|
43504
43537
|
severity: "warn",
|
|
@@ -43856,6 +43889,40 @@ var LINE_MATCHERS = [
|
|
|
43856
43889
|
function includesPhraseAtWordStart(haystack, phrase) {
|
|
43857
43890
|
return new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(haystack);
|
|
43858
43891
|
}
|
|
43892
|
+
function foldForCopy(text2) {
|
|
43893
|
+
return text2.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
43894
|
+
}
|
|
43895
|
+
function detectConcessions(text2) {
|
|
43896
|
+
const folded = foldForCopy(text2);
|
|
43897
|
+
const out = [];
|
|
43898
|
+
for (const pattern of CONCESSION_PATTERNS) {
|
|
43899
|
+
for (const m of folded.matchAll(pattern)) {
|
|
43900
|
+
const index = m.index ?? 0;
|
|
43901
|
+
if (PRICE_CONCESSION_CONTEXT.test(sentenceAround(folded, index))) continue;
|
|
43902
|
+
if (CONCESSION_NEGATION_CONTEXT.test(clauseBefore(folded, index))) continue;
|
|
43903
|
+
out.push({
|
|
43904
|
+
index,
|
|
43905
|
+
sample: text2.slice(index, index + 70).replace(/\s+/g, " ").trim()
|
|
43906
|
+
});
|
|
43907
|
+
}
|
|
43908
|
+
}
|
|
43909
|
+
return out;
|
|
43910
|
+
}
|
|
43911
|
+
function sentenceAround(text2, index) {
|
|
43912
|
+
const rest = text2.slice(index).search(/[.!?\n]/);
|
|
43913
|
+
return text2.slice(sentenceStart(text2, index), rest === -1 ? text2.length : index + rest);
|
|
43914
|
+
}
|
|
43915
|
+
function clauseBefore(text2, index) {
|
|
43916
|
+
const prefix = text2.slice(sentenceStart(text2, index), index);
|
|
43917
|
+
const lastBreak = Math.max(prefix.lastIndexOf(","), prefix.lastIndexOf(";"), prefix.lastIndexOf(":"));
|
|
43918
|
+
return prefix.slice(lastBreak + 1);
|
|
43919
|
+
}
|
|
43920
|
+
function sentenceStart(text2, index) {
|
|
43921
|
+
for (let i = index; i > 0; i--) {
|
|
43922
|
+
if (/[.!?\n]/.test(text2[i - 1] ?? "")) return i;
|
|
43923
|
+
}
|
|
43924
|
+
return 0;
|
|
43925
|
+
}
|
|
43859
43926
|
function stripHtmlToText(html) {
|
|
43860
43927
|
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ");
|
|
43861
43928
|
}
|
|
@@ -43867,17 +43934,76 @@ function stripHtmlPreservingHeadings(html) {
|
|
|
43867
43934
|
`
|
|
43868
43935
|
).replace(/<\/(?:p|div|section|li|tr|br)>|<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n");
|
|
43869
43936
|
}
|
|
43937
|
+
function isProseLiteral(value) {
|
|
43938
|
+
const text2 = value.trim();
|
|
43939
|
+
if (text2.length < 12) return false;
|
|
43940
|
+
if (/^(?:https?:\/\/|\/|\.{1,2}\/|#|@|[a-z]+:)/i.test(text2)) return false;
|
|
43941
|
+
if (!new RegExp("\\p{L}", "u").test(text2)) return false;
|
|
43942
|
+
const words2 = text2.split(/\s+/).filter(Boolean);
|
|
43943
|
+
if (words2.length < 4) return false;
|
|
43944
|
+
const classy = words2.filter((w) => /^-?[a-z][a-z0-9]*(?:[-.:/][a-z0-9.[\]%-]+)+$/.test(w)).length;
|
|
43945
|
+
if (classy >= 3 && classy / words2.length > 0.5) return false;
|
|
43946
|
+
return true;
|
|
43947
|
+
}
|
|
43870
43948
|
function blankAstroFrontmatter(text2) {
|
|
43871
|
-
|
|
43949
|
+
const cut = frontmatterLength(text2);
|
|
43950
|
+
if (cut === 0) return text2;
|
|
43951
|
+
const head = text2.slice(0, cut);
|
|
43952
|
+
const kept = head.split("").map((ch) => ch === "\n" ? "\n" : " ");
|
|
43953
|
+
for (const literal of stringLiterals(head)) {
|
|
43954
|
+
if (!isProseLiteral(literal.value)) continue;
|
|
43955
|
+
for (let i = 0; i < literal.value.length; i++) {
|
|
43956
|
+
const at = literal.start + i;
|
|
43957
|
+
if (head[at] !== "\n") kept[at] = head[at] ?? " ";
|
|
43958
|
+
}
|
|
43959
|
+
}
|
|
43960
|
+
return kept.join("") + text2.slice(cut);
|
|
43961
|
+
}
|
|
43962
|
+
function frontmatterLength(text2) {
|
|
43963
|
+
if (!text2.startsWith("---")) return 0;
|
|
43872
43964
|
const lines = text2.split("\n");
|
|
43873
|
-
if (!/^---\s*$/.test(lines[0] ?? "")) return
|
|
43874
|
-
|
|
43875
|
-
|
|
43876
|
-
|
|
43877
|
-
|
|
43965
|
+
if (!/^---\s*$/.test(lines[0] ?? "")) return 0;
|
|
43966
|
+
const end = lines.findIndex((line, i) => i > 0 && /^---\s*$/.test(line));
|
|
43967
|
+
return end === -1 ? 0 : lines.slice(0, end + 1).join("\n").length;
|
|
43968
|
+
}
|
|
43969
|
+
function stringLiterals(source) {
|
|
43970
|
+
const out = [];
|
|
43971
|
+
for (let i = 0; i < source.length; i++) {
|
|
43972
|
+
const commentEnd = skipComment(source, i);
|
|
43973
|
+
if (commentEnd !== -1) {
|
|
43974
|
+
i = commentEnd;
|
|
43975
|
+
continue;
|
|
43878
43976
|
}
|
|
43977
|
+
const quote = source[i];
|
|
43978
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") continue;
|
|
43979
|
+
const close = closingQuote(source, i + 1, quote);
|
|
43980
|
+
if (close === -1) break;
|
|
43981
|
+
out.push({ start: i + 1, value: source.slice(i + 1, close) });
|
|
43982
|
+
i = close;
|
|
43879
43983
|
}
|
|
43880
|
-
return
|
|
43984
|
+
return out;
|
|
43985
|
+
}
|
|
43986
|
+
function skipComment(source, i) {
|
|
43987
|
+
if (source[i] !== "/") return -1;
|
|
43988
|
+
if (source[i + 1] === "/") {
|
|
43989
|
+
const nl = source.indexOf("\n", i);
|
|
43990
|
+
return nl === -1 ? source.length : nl;
|
|
43991
|
+
}
|
|
43992
|
+
if (source[i + 1] === "*") {
|
|
43993
|
+
const close = source.indexOf("*/", i + 2);
|
|
43994
|
+
return close === -1 ? source.length : close + 1;
|
|
43995
|
+
}
|
|
43996
|
+
return -1;
|
|
43997
|
+
}
|
|
43998
|
+
function closingQuote(source, from, quote) {
|
|
43999
|
+
for (let j = from; j < source.length; j++) {
|
|
44000
|
+
if (source[j] === "\\") {
|
|
44001
|
+
j++;
|
|
44002
|
+
continue;
|
|
44003
|
+
}
|
|
44004
|
+
if (source[j] === quote) return j;
|
|
44005
|
+
}
|
|
44006
|
+
return -1;
|
|
43881
44007
|
}
|
|
43882
44008
|
var ANALYZERS = [
|
|
43883
44009
|
// Overused primary font (font-family declarations + Google Fonts URLs). Fires
|
|
@@ -44068,6 +44194,23 @@ var ANALYZERS = [
|
|
|
44068
44194
|
return out;
|
|
44069
44195
|
}
|
|
44070
44196
|
},
|
|
44197
|
+
// Positioning concessions. File-scope (fires at 1, and the line is what the
|
|
44198
|
+
// agent needs to fix it) and read from the visible copy only.
|
|
44199
|
+
{
|
|
44200
|
+
scope: "file",
|
|
44201
|
+
run: (text2, file) => {
|
|
44202
|
+
const visible = stripHtmlPreservingHeadings(text2);
|
|
44203
|
+
return detectConcessions(visible).map(({ sample: sample2 }) => {
|
|
44204
|
+
const at = text2.indexOf(sample2.slice(0, 24));
|
|
44205
|
+
return {
|
|
44206
|
+
id: "competitor-concession",
|
|
44207
|
+
snippet: `"${sample2}"`,
|
|
44208
|
+
file,
|
|
44209
|
+
...at >= 0 ? { line: lineOf(text2, at) } : {}
|
|
44210
|
+
};
|
|
44211
|
+
});
|
|
44212
|
+
}
|
|
44213
|
+
},
|
|
44071
44214
|
// Gradient text in authored CSS. A rule block spans lines, so this scans a
|
|
44072
44215
|
// ±6-line window around each `background-clip: text` instead of one line
|
|
44073
44216
|
// (the Tailwind class form stays a line matcher above).
|
|
@@ -44161,6 +44304,72 @@ function lineOf(text2, index) {
|
|
|
44161
44304
|
for (let i = 0; i < index && i < text2.length; i++) if (text2[i] === "\n") line++;
|
|
44162
44305
|
return line;
|
|
44163
44306
|
}
|
|
44307
|
+
var COMPARISON_MARKERS = /\b(?:vs|versus|frente a|comparad|compara|mejor|mejores|peor|peores|alternativa|melhor|melhores|pior|piores|compared|better|worse|alternative|than)\b/;
|
|
44308
|
+
function tableRanges(text2) {
|
|
44309
|
+
const out = [];
|
|
44310
|
+
for (const m of text2.matchAll(/<table\b[\s\S]*?<\/table>/gi)) {
|
|
44311
|
+
const start = m.index ?? 0;
|
|
44312
|
+
out.push([start, start + m[0].length]);
|
|
44313
|
+
}
|
|
44314
|
+
return out;
|
|
44315
|
+
}
|
|
44316
|
+
function detectCompetitorMentions(sources, competitors) {
|
|
44317
|
+
const names = distinctCompetitorNames(competitors);
|
|
44318
|
+
if (names.length === 0) return [];
|
|
44319
|
+
const out = [];
|
|
44320
|
+
for (const source of sources) out.push(...mentionsInSource(source, names));
|
|
44321
|
+
return dedupe(out);
|
|
44322
|
+
}
|
|
44323
|
+
function distinctCompetitorNames(competitors) {
|
|
44324
|
+
const byFold = /* @__PURE__ */ new Map();
|
|
44325
|
+
for (const raw of competitors) {
|
|
44326
|
+
if (typeof raw !== "string") continue;
|
|
44327
|
+
const name = raw.trim();
|
|
44328
|
+
if (name.length < 4) continue;
|
|
44329
|
+
const key = foldForCopy(name);
|
|
44330
|
+
if (!byFold.has(key) || new RegExp("\\p{Lu}", "u").test(name)) byFold.set(key, name);
|
|
44331
|
+
}
|
|
44332
|
+
return [...byFold.values()];
|
|
44333
|
+
}
|
|
44334
|
+
function mentionsInSource(source, names) {
|
|
44335
|
+
const text2 = blankAstroFrontmatter(source.text);
|
|
44336
|
+
const visible = blankHtmlKeepingOffsets(text2);
|
|
44337
|
+
const tables = tableRanges(text2);
|
|
44338
|
+
const out = [];
|
|
44339
|
+
for (const name of names) {
|
|
44340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
44341
|
+
for (const m of visible.matchAll(new RegExp(`\\b${escapeRegExp(name)}\\b`, "gi"))) {
|
|
44342
|
+
const at = m.index ?? 0;
|
|
44343
|
+
if (tables.some(([from, to]) => at >= from && at < to)) continue;
|
|
44344
|
+
const start = sentenceStart(visible, at);
|
|
44345
|
+
if (seen.has(start)) continue;
|
|
44346
|
+
const sentence = sentenceAround(visible, at).replace(/\s+/g, " ").trim();
|
|
44347
|
+
if (!COMPARISON_MARKERS.test(foldForCopy(sentence))) continue;
|
|
44348
|
+
seen.add(start);
|
|
44349
|
+
out.push({
|
|
44350
|
+
id: "competitor-named-in-comparison",
|
|
44351
|
+
snippet: `"${sentence.slice(0, 70)}" names ${name}`,
|
|
44352
|
+
file: source.path,
|
|
44353
|
+
line: lineOf(text2, at)
|
|
44354
|
+
});
|
|
44355
|
+
}
|
|
44356
|
+
}
|
|
44357
|
+
return out;
|
|
44358
|
+
}
|
|
44359
|
+
function blankHtmlKeepingOffsets(text2) {
|
|
44360
|
+
const out = text2.split("");
|
|
44361
|
+
const pattern = /<script\b[^>]*>[\s\S]*?<\/script>|<style\b[^>]*>[\s\S]*?<\/style>|<!--[\s\S]*?-->|<[^>]+>/gi;
|
|
44362
|
+
for (const m of text2.matchAll(pattern)) {
|
|
44363
|
+
const start = m.index ?? 0;
|
|
44364
|
+
for (let i = start; i < start + m[0].length && i < out.length; i++) {
|
|
44365
|
+
if (out[i] !== "\n") out[i] = " ";
|
|
44366
|
+
}
|
|
44367
|
+
}
|
|
44368
|
+
return out.join("");
|
|
44369
|
+
}
|
|
44370
|
+
function escapeRegExp(value) {
|
|
44371
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44372
|
+
}
|
|
44164
44373
|
function detectSource(source) {
|
|
44165
44374
|
const file = source.path;
|
|
44166
44375
|
const text2 = blankAstroFrontmatter(source.text);
|
|
@@ -44220,6 +44429,7 @@ function critiqueLanding(input) {
|
|
|
44220
44429
|
const raws = [];
|
|
44221
44430
|
for (const source of sources) raws.push(...detectSource(source));
|
|
44222
44431
|
raws.push(...detectPage(sources));
|
|
44432
|
+
raws.push(...detectCompetitorMentions(sources, input.competitors ?? []));
|
|
44223
44433
|
for (const raw of raws) {
|
|
44224
44434
|
const meta = RULE_META[raw.id];
|
|
44225
44435
|
if (!meta) continue;
|
|
@@ -44260,7 +44470,7 @@ function describeCounts(findings) {
|
|
|
44260
44470
|
// src/commands/landing/snapshot.ts
|
|
44261
44471
|
import { mkdir as mkdir8, rename as rename2, writeFile as writeFile12 } from "fs/promises";
|
|
44262
44472
|
import path26 from "path";
|
|
44263
|
-
var CRITIC_VERSION = "
|
|
44473
|
+
var CRITIC_VERSION = "3";
|
|
44264
44474
|
function critiqueCacheDir(projectRoot) {
|
|
44265
44475
|
return path26.join(projectRoot, ".cache", "landing-critique");
|
|
44266
44476
|
}
|
|
@@ -44334,7 +44544,7 @@ async function walkAstro(dir) {
|
|
|
44334
44544
|
// src/commands/landing/critique.ts
|
|
44335
44545
|
registerSchema({
|
|
44336
44546
|
command: "landing.critique",
|
|
44337
|
-
description: "Start here: `baker landing critique my-landing` after building or editing a landing. Deterministic design-quality critic (ADVISORY \u2014 findings never fail it). Flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images\u2026) tiered block/warn/advisory, and records the critique the publish quality gate requires.",
|
|
44547
|
+
description: "Start here: `baker landing critique my-landing` after building or editing a landing. Deterministic design-quality critic (ADVISORY \u2014 findings never fail it). Flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images\u2026) plus positioning integrity (copy that ranks a competitor above the client, a rival named in a comparison) tiered block/warn/advisory, and records the critique the publish quality gate requires.",
|
|
44338
44548
|
args: {
|
|
44339
44549
|
slug: {
|
|
44340
44550
|
type: "string",
|
|
@@ -44364,7 +44574,7 @@ function fail5(code, message, fix) {
|
|
|
44364
44574
|
var critiqueCommand2 = defineCommand158({
|
|
44365
44575
|
meta: {
|
|
44366
44576
|
name: "critique",
|
|
44367
|
-
description: "Start here: `baker landing critique <slug>` after building or editing a landing. Deterministic design-quality critic (ADVISORY \u2014 findings never fail it). Flags the known AI design tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) tiered block/warn/advisory, respecting the client's BRAND.md as the allowlist. Also records the critique that publishing requires \u2014 run it before finishing a landing."
|
|
44577
|
+
description: "Start here: `baker landing critique <slug>` after building or editing a landing. Deterministic design-quality critic (ADVISORY \u2014 findings never fail it). Flags the known AI design tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) and positioning integrity (a concession ranking a competitor above the client, a rival named in a comparison) tiered block/warn/advisory, respecting the client's BRAND.md as the allowlist. Also records the critique that publishing requires \u2014 run it before finishing a landing."
|
|
44368
44578
|
},
|
|
44369
44579
|
args: {
|
|
44370
44580
|
slug: {
|
|
@@ -44392,7 +44602,8 @@ var critiqueCommand2 = defineCommand158({
|
|
|
44392
44602
|
}
|
|
44393
44603
|
}
|
|
44394
44604
|
const brand = await loadBrandTokens(projectRoot);
|
|
44395
|
-
const
|
|
44605
|
+
const competitors = await readCompetitorNames(projectRoot);
|
|
44606
|
+
const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, competitors)));
|
|
44396
44607
|
const landings = results.map(({ slug, report }) => ({
|
|
44397
44608
|
slug,
|
|
44398
44609
|
overall: report.overall,
|
|
@@ -44423,10 +44634,10 @@ var critiqueCommand2 = defineCommand158({
|
|
|
44423
44634
|
);
|
|
44424
44635
|
}
|
|
44425
44636
|
});
|
|
44426
|
-
async function critiqueOne(projectRoot, slug, brand) {
|
|
44637
|
+
async function critiqueOne(projectRoot, slug, brand, competitors) {
|
|
44427
44638
|
const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
|
|
44428
44639
|
const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
|
|
44429
|
-
const report = critiqueLanding({ slug, sources, brand });
|
|
44640
|
+
const report = critiqueLanding({ slug, sources, brand, competitors });
|
|
44430
44641
|
let snapshotFailed = false;
|
|
44431
44642
|
try {
|
|
44432
44643
|
await writeCritiqueSnapshot(projectRoot, {
|
|
@@ -44442,6 +44653,26 @@ async function critiqueOne(projectRoot, slug, brand) {
|
|
|
44442
44653
|
}
|
|
44443
44654
|
return { slug, report, snapshotFailed };
|
|
44444
44655
|
}
|
|
44656
|
+
async function readCompetitorNames(projectRoot) {
|
|
44657
|
+
const dir = path28.join(projectRoot, "src", "content", "competitors");
|
|
44658
|
+
let entries;
|
|
44659
|
+
try {
|
|
44660
|
+
entries = (await readdir9(dir)).filter((f) => f.endsWith(".md"));
|
|
44661
|
+
} catch {
|
|
44662
|
+
return [];
|
|
44663
|
+
}
|
|
44664
|
+
const names = [];
|
|
44665
|
+
for (const entry of entries) {
|
|
44666
|
+
names.push(path28.basename(entry, ".md").replace(/[-_]+/g, " "));
|
|
44667
|
+
try {
|
|
44668
|
+
const head = (await readFile22(path28.join(dir, entry), "utf8")).slice(0, 2e3);
|
|
44669
|
+
const titled = /^\s*(?:title|name)\s*:\s*["']?([^"'\n]+)["']?\s*$/im.exec(head);
|
|
44670
|
+
if (titled?.[1]) names.push(titled[1].trim());
|
|
44671
|
+
} catch {
|
|
44672
|
+
}
|
|
44673
|
+
}
|
|
44674
|
+
return names;
|
|
44675
|
+
}
|
|
44445
44676
|
async function listLandingSlugs(projectRoot) {
|
|
44446
44677
|
try {
|
|
44447
44678
|
const entries = await readdir9(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
@@ -47645,7 +47876,7 @@ Start here: \`baker landing critique <slug>\` after building or editing a landin
|
|
|
47645
47876
|
|
|
47646
47877
|
Subcommands:
|
|
47647
47878
|
baker landing inspiration \u2014 reference library of real landing-page sections; search it during research, BEFORE writing the Direction Contract. Inspiration only: take the mechanism, never the words.
|
|
47648
|
-
baker landing critique <slug> \u2014 deterministic design-quality critic (advisory): flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) tiered block/warn/advisory, respecting the client's BRAND.md. Records the critique the publish quality gate requires \u2014 run it before finishing a landing.`
|
|
47879
|
+
baker landing critique <slug> \u2014 deterministic design-quality critic (advisory): flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) and positioning integrity (copy ranking a competitor above the client) tiered block/warn/advisory, respecting the client's BRAND.md. Records the critique the publish quality gate requires \u2014 run it before finishing a landing.`
|
|
47649
47880
|
},
|
|
47650
47881
|
subCommands: {
|
|
47651
47882
|
inspiration: inspirationCommand,
|
|
@@ -49389,7 +49620,7 @@ var listCommand15 = defineCommand185({
|
|
|
49389
49620
|
});
|
|
49390
49621
|
|
|
49391
49622
|
// src/commands/scheduled-actions/templates.ts
|
|
49392
|
-
import { readFile as
|
|
49623
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
49393
49624
|
import path36 from "path";
|
|
49394
49625
|
import { defineCommand as defineCommand186 } from "citty";
|
|
49395
49626
|
registerSchema({
|
|
@@ -49493,7 +49724,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
49493
49724
|
}
|
|
49494
49725
|
if (save.length > 0) {
|
|
49495
49726
|
const briefFile = flag("brief-file");
|
|
49496
|
-
const brief = briefFile.length > 0 ? await
|
|
49727
|
+
const brief = briefFile.length > 0 ? await readFile23(path36.resolve(briefFile), "utf8") : flag("brief");
|
|
49497
49728
|
if (brief.trim().length === 0) {
|
|
49498
49729
|
failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
|
|
49499
49730
|
}
|
|
@@ -52676,7 +52907,7 @@ var searchCommand4 = defineCommand213({
|
|
|
52676
52907
|
var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
52677
52908
|
|
|
52678
52909
|
// src/commands/videos/upload.ts
|
|
52679
|
-
import { readFile as
|
|
52910
|
+
import { readFile as readFile24, stat as stat8 } from "fs/promises";
|
|
52680
52911
|
import { basename as basename3, extname as extname4 } from "path";
|
|
52681
52912
|
import { defineCommand as defineCommand214 } from "citty";
|
|
52682
52913
|
var MIME_MAP = {
|
|
@@ -52773,7 +53004,7 @@ var uploadCommand2 = defineCommand214({
|
|
|
52773
53004
|
originalFilename,
|
|
52774
53005
|
descriptionContext
|
|
52775
53006
|
});
|
|
52776
|
-
const fileBuffer = await
|
|
53007
|
+
const fileBuffer = await readFile24(filePath);
|
|
52777
53008
|
const uploadResponse = await fetch(uploadUrl, {
|
|
52778
53009
|
method: "PUT",
|
|
52779
53010
|
headers: { "Content-Type": contentType },
|