@koda-sl/baker-cli 0.274.0-dev.bbba3d7fc → 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/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-VWLNPPC6.js";
61
+ } from "./chunk-36KVUYVL.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -12459,22 +12459,7 @@ var ANALYTICS_EVENT_TYPES = [
12459
12459
  * know who this was" unanswerable.
12460
12460
  */
12461
12461
  "identify",
12462
- /**
12463
- * A named event: an escape hatch a page defines, or one a client's own server
12464
- * posted to `POST /v1/events`.
12465
- *
12466
- * One type for both, deliberately. There was briefly a `server_event`, and
12467
- * the type was the mistake: every report that shows named events whitelists
12468
- * `custom`, so a server-sent "Deal won" was structurally invisible on the
12469
- * Page events card, in Top pages and in the events catalogue — the three
12470
- * screens somebody would look at after being told it had arrived. GA4 makes
12471
- * the same call for the same reason: a Measurement Protocol event is just an
12472
- * event, and the transport is a property.
12473
- *
12474
- * Which collector wrote it is {@link ANALYTICS_SOURCES} — `client` for a
12475
- * page, `ingest` for a third party's server — and every report that needs to
12476
- * tell them apart filters on that.
12477
- */
12462
+ /** Escape hatch for a page-specific event. */
12478
12463
  "custom"
12479
12464
  ];
12480
12465
  var ANALYTICS_DEVICE_TYPES = ["desktop", "mobile", "tablet", "bot", "unknown"];
@@ -12997,19 +12982,6 @@ var analyticsQueryRequestSchema = z25.object({
12997
12982
  * superset and the right default for "how is paid doing".
12998
12983
  */
12999
12984
  adPlatform: z25.enum(AD_PLATFORMS).optional(),
13000
- /**
13001
- * Read only what one collector produced: `site` for everything Baker
13002
- * measured itself, `systems` for what a client's own server posted to
13003
- * `POST /v1/events`.
13004
- *
13005
- * Omit it for all of it, which is the default and the honest superset.
13006
- * Only the reads where the split is a question somebody asks honour it —
13007
- * the totals, the trend, the two page-event reads, the conversion
13008
- * breakdown and the events catalogue. A session-scoped report cannot: an
13009
- * ingest row carries no `session_id`, deliberately, so there is no visit
13010
- * for it to be counted in.
13011
- */
13012
- eventOrigin: z25.enum(["site", "systems"]).optional(),
13013
12985
  /** Return the long breakdowns and the per-day trend. */
13014
12986
  full: z25.boolean().optional(),
13015
12987
  /**
@@ -13282,7 +13254,6 @@ var analyticsTagRowSchema = z25.object({
13282
13254
  /** Converting visits per entrance. */
13283
13255
  conversionRate: z25.number().min(0).max(1).nullable()
13284
13256
  });
13285
- var ANALYTICS_EVENT_ORIGINS = ["site", "systems", "both"];
13286
13257
  var analyticsCustomEventRowSchema = z25.object({
13287
13258
  eventName: z25.string(),
13288
13259
  events: z25.number().int().nonnegative(),
@@ -13298,9 +13269,7 @@ var analyticsCustomEventRowSchema = z25.object({
13298
13269
  */
13299
13270
  propertyKeys: z25.array(z25.string()),
13300
13271
  valueTotal: z25.number(),
13301
- landings: z25.array(z25.string()),
13302
- /** Which collector produced this name — and `both` when more than one did. */
13303
- origin: z25.enum(ANALYTICS_EVENT_ORIGINS)
13272
+ landings: z25.array(z25.string())
13304
13273
  });
13305
13274
  var analyticsEventBreakdownRowSchema = z25.object({
13306
13275
  value: z25.string(),
@@ -13635,16 +13604,6 @@ var analyticsConversionRowSchema = z25.object({
13635
13604
  var analyticsEventCatalogRowSchema = z25.object({
13636
13605
  /** `form:…` | `submit:…` | `page:…` | `exit:…` — parse with `parseEventKey`. */
13637
13606
  eventKey: z25.string(),
13638
- /**
13639
- * Which collector produced this key — and `both` when more than one did.
13640
- *
13641
- * On the row rather than in the key, which is the whole shape of the change
13642
- * that removed the `server:` family: a picker can say where an event came
13643
- * from without a filter round-trip, and a report can still separate the two
13644
- * populations at any grain, but nothing downstream has to learn a second
13645
- * vocabulary to count one.
13646
- */
13647
- origin: z25.enum(ANALYTICS_EVENT_ORIGINS),
13648
13607
  events: z25.number(),
13649
13608
  sessions: z25.number(),
13650
13609
  /** The Form node's kind, for a `form:` key. Empty otherwise. */
@@ -43331,7 +43290,7 @@ Full guide: __tooling__/docs/tools/baker/images.md`
43331
43290
  import { defineCommand as defineCommand168 } from "citty";
43332
43291
 
43333
43292
  // src/commands/landing/critique.ts
43334
- import { readdir as readdir9, stat as stat6 } from "fs/promises";
43293
+ import { readdir as readdir9, readFile as readFile22, stat as stat6 } from "fs/promises";
43335
43294
  import path28 from "path";
43336
43295
  import { defineCommand as defineCommand158 } from "citty";
43337
43296
 
@@ -43461,6 +43420,29 @@ var COPY_TELL_GATES = {
43461
43420
  "inline-header-list": { minCount: 3, severity: "advisory" },
43462
43421
  emoji: { minCount: 3, severity: "advisory" }
43463
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/;
43464
43446
  var AUTONOMY_CLAIMS = [
43465
43447
  "fully autonomous",
43466
43448
  "completely autonomous",
@@ -43540,6 +43522,16 @@ var RULE_META = {
43540
43522
  severity: "block",
43541
43523
  note: "An <img> with an empty or placeholder src ships a broken image. Source a real asset via `baker images`."
43542
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
+ },
43543
43535
  "side-tab": {
43544
43536
  family: "borders_depth",
43545
43537
  severity: "warn",
@@ -43897,6 +43889,40 @@ var LINE_MATCHERS = [
43897
43889
  function includesPhraseAtWordStart(haystack, phrase) {
43898
43890
  return new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(haystack);
43899
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
+ }
43900
43926
  function stripHtmlToText(html) {
43901
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, " ");
43902
43928
  }
@@ -43908,17 +43934,76 @@ function stripHtmlPreservingHeadings(html) {
43908
43934
  `
43909
43935
  ).replace(/<\/(?:p|div|section|li|tr|br)>|<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n");
43910
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
+ }
43911
43948
  function blankAstroFrontmatter(text2) {
43912
- if (!text2.startsWith("---")) return text2;
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;
43913
43964
  const lines = text2.split("\n");
43914
- if (!/^---\s*$/.test(lines[0] ?? "")) return text2;
43915
- for (let i = 1; i < lines.length; i++) {
43916
- if (/^---\s*$/.test(lines[i] ?? "")) {
43917
- for (let j = 0; j <= i; j++) lines[j] = "";
43918
- return lines.join("\n");
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;
43919
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;
43920
43983
  }
43921
- return text2;
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;
43922
44007
  }
43923
44008
  var ANALYZERS = [
43924
44009
  // Overused primary font (font-family declarations + Google Fonts URLs). Fires
@@ -44109,6 +44194,23 @@ var ANALYZERS = [
44109
44194
  return out;
44110
44195
  }
44111
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
+ },
44112
44214
  // Gradient text in authored CSS. A rule block spans lines, so this scans a
44113
44215
  // ±6-line window around each `background-clip: text` instead of one line
44114
44216
  // (the Tailwind class form stays a line matcher above).
@@ -44202,6 +44304,72 @@ function lineOf(text2, index) {
44202
44304
  for (let i = 0; i < index && i < text2.length; i++) if (text2[i] === "\n") line++;
44203
44305
  return line;
44204
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
+ }
44205
44373
  function detectSource(source) {
44206
44374
  const file = source.path;
44207
44375
  const text2 = blankAstroFrontmatter(source.text);
@@ -44261,6 +44429,7 @@ function critiqueLanding(input) {
44261
44429
  const raws = [];
44262
44430
  for (const source of sources) raws.push(...detectSource(source));
44263
44431
  raws.push(...detectPage(sources));
44432
+ raws.push(...detectCompetitorMentions(sources, input.competitors ?? []));
44264
44433
  for (const raw of raws) {
44265
44434
  const meta = RULE_META[raw.id];
44266
44435
  if (!meta) continue;
@@ -44301,7 +44470,7 @@ function describeCounts(findings) {
44301
44470
  // src/commands/landing/snapshot.ts
44302
44471
  import { mkdir as mkdir8, rename as rename2, writeFile as writeFile12 } from "fs/promises";
44303
44472
  import path26 from "path";
44304
- var CRITIC_VERSION = "2";
44473
+ var CRITIC_VERSION = "3";
44305
44474
  function critiqueCacheDir(projectRoot) {
44306
44475
  return path26.join(projectRoot, ".cache", "landing-critique");
44307
44476
  }
@@ -44375,7 +44544,7 @@ async function walkAstro(dir) {
44375
44544
  // src/commands/landing/critique.ts
44376
44545
  registerSchema({
44377
44546
  command: "landing.critique",
44378
- 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.",
44379
44548
  args: {
44380
44549
  slug: {
44381
44550
  type: "string",
@@ -44405,7 +44574,7 @@ function fail5(code, message, fix) {
44405
44574
  var critiqueCommand2 = defineCommand158({
44406
44575
  meta: {
44407
44576
  name: "critique",
44408
- 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."
44409
44578
  },
44410
44579
  args: {
44411
44580
  slug: {
@@ -44433,7 +44602,8 @@ var critiqueCommand2 = defineCommand158({
44433
44602
  }
44434
44603
  }
44435
44604
  const brand = await loadBrandTokens(projectRoot);
44436
- const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
44605
+ const competitors = await readCompetitorNames(projectRoot);
44606
+ const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, competitors)));
44437
44607
  const landings = results.map(({ slug, report }) => ({
44438
44608
  slug,
44439
44609
  overall: report.overall,
@@ -44464,10 +44634,10 @@ var critiqueCommand2 = defineCommand158({
44464
44634
  );
44465
44635
  }
44466
44636
  });
44467
- async function critiqueOne(projectRoot, slug, brand) {
44637
+ async function critiqueOne(projectRoot, slug, brand, competitors) {
44468
44638
  const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
44469
44639
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
44470
- const report = critiqueLanding({ slug, sources, brand });
44640
+ const report = critiqueLanding({ slug, sources, brand, competitors });
44471
44641
  let snapshotFailed = false;
44472
44642
  try {
44473
44643
  await writeCritiqueSnapshot(projectRoot, {
@@ -44483,6 +44653,26 @@ async function critiqueOne(projectRoot, slug, brand) {
44483
44653
  }
44484
44654
  return { slug, report, snapshotFailed };
44485
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
+ }
44486
44676
  async function listLandingSlugs(projectRoot) {
44487
44677
  try {
44488
44678
  const entries = await readdir9(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
@@ -47686,7 +47876,7 @@ Start here: \`baker landing critique <slug>\` after building or editing a landin
47686
47876
 
47687
47877
  Subcommands:
47688
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.
47689
- 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.`
47690
47880
  },
47691
47881
  subCommands: {
47692
47882
  inspiration: inspirationCommand,
@@ -49430,7 +49620,7 @@ var listCommand15 = defineCommand185({
49430
49620
  });
49431
49621
 
49432
49622
  // src/commands/scheduled-actions/templates.ts
49433
- import { readFile as readFile22 } from "fs/promises";
49623
+ import { readFile as readFile23 } from "fs/promises";
49434
49624
  import path36 from "path";
49435
49625
  import { defineCommand as defineCommand186 } from "citty";
49436
49626
  registerSchema({
@@ -49534,7 +49724,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
49534
49724
  }
49535
49725
  if (save.length > 0) {
49536
49726
  const briefFile = flag("brief-file");
49537
- const brief = briefFile.length > 0 ? await readFile22(path36.resolve(briefFile), "utf8") : flag("brief");
49727
+ const brief = briefFile.length > 0 ? await readFile23(path36.resolve(briefFile), "utf8") : flag("brief");
49538
49728
  if (brief.trim().length === 0) {
49539
49729
  failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
49540
49730
  }
@@ -52717,7 +52907,7 @@ var searchCommand4 = defineCommand213({
52717
52907
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
52718
52908
 
52719
52909
  // src/commands/videos/upload.ts
52720
- import { readFile as readFile23, stat as stat8 } from "fs/promises";
52910
+ import { readFile as readFile24, stat as stat8 } from "fs/promises";
52721
52911
  import { basename as basename3, extname as extname4 } from "path";
52722
52912
  import { defineCommand as defineCommand214 } from "citty";
52723
52913
  var MIME_MAP = {
@@ -52814,7 +53004,7 @@ var uploadCommand2 = defineCommand214({
52814
53004
  originalFilename,
52815
53005
  descriptionContext
52816
53006
  });
52817
- const fileBuffer = await readFile23(filePath);
53007
+ const fileBuffer = await readFile24(filePath);
52818
53008
  const uploadResponse = await fetch(uploadUrl, {
52819
53009
  method: "PUT",
52820
53010
  headers: { "Content-Type": contentType },