@koda-sl/baker-cli 0.328.0 → 0.330.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
@@ -578,12 +578,24 @@ var registry = /* @__PURE__ */ new Map();
578
578
  function registerSchema(schema) {
579
579
  registry.set(schema.command, schema);
580
580
  }
581
- function getSchema(command) {
582
- return registry.get(command);
583
- }
584
581
  function listSchemas() {
585
582
  return [...registry.keys()];
586
583
  }
584
+ function spellingKey(name) {
585
+ return name.toLowerCase().replaceAll("-", "");
586
+ }
587
+ function resolveSchemaName(words2, names) {
588
+ const joined = words2.join(".");
589
+ if (names.includes(joined)) {
590
+ return joined;
591
+ }
592
+ const matches = names.filter((name) => spellingKey(name) === spellingKey(joined));
593
+ return matches.length === 1 ? matches[0] : void 0;
594
+ }
595
+ function findSchema(words2) {
596
+ const name = resolveSchemaName(words2, listSchemas());
597
+ return name === void 0 ? void 0 : registry.get(name);
598
+ }
587
599
 
588
600
  // ../api/src/actions.ts
589
601
  import { z } from "zod";
@@ -1626,11 +1638,11 @@ var trackingParamsSetSchema = z3.object({
1626
1638
  message: `${both.join(", ")} is set as both a dynamic and a fixed parameter \u2014 LinkedIn appends each key once, so keep only one`
1627
1639
  });
1628
1640
  }
1629
- const count = Object.keys(p.dynamicValueParameters ?? {}).length + Object.keys(p.customValueParameters ?? {}).length;
1630
- if (count > LINKEDIN_LIMITS.trackingParams.parametersMax) {
1641
+ const count2 = Object.keys(p.dynamicValueParameters ?? {}).length + Object.keys(p.customValueParameters ?? {}).length;
1642
+ if (count2 > LINKEDIN_LIMITS.trackingParams.parametersMax) {
1631
1643
  ctx.addIssue({
1632
1644
  code: "custom",
1633
- message: `${count} parameters \u2014 keep it under ${LINKEDIN_LIMITS.trackingParams.parametersMax}`
1645
+ message: `${count2} parameters \u2014 keep it under ${LINKEDIN_LIMITS.trackingParams.parametersMax}`
1634
1646
  });
1635
1647
  }
1636
1648
  });
@@ -1937,8 +1949,8 @@ var leadFormQuestionSchema = z3.object({
1937
1949
  });
1938
1950
  }
1939
1951
  if (q.questionType === "MULTIPLE_CHOICE") {
1940
- const count = q.options?.length ?? 0;
1941
- if (count < LEAD.choiceOptionsMin || count > LEAD.choiceOptionsMax) {
1952
+ const count2 = q.options?.length ?? 0;
1953
+ if (count2 < LEAD.choiceOptionsMin || count2 > LEAD.choiceOptionsMax) {
1942
1954
  ctx.addIssue({
1943
1955
  code: "custom",
1944
1956
  path: ["options"],
@@ -10672,23 +10684,23 @@ var FALSE_RANGE_PATTERNS = [
10672
10684
  /\bdesde\s+([^,.;]{3,30}?)\s+hasta\s+([^,.;]{3,30}?)(?=[,.;]|$)/gi
10673
10685
  ];
10674
10686
  function countFalseRanges(text2) {
10675
- let count = 0;
10687
+ let count2 = 0;
10676
10688
  let first = "";
10677
10689
  for (const re2 of FALSE_RANGE_PATTERNS) {
10678
10690
  for (const m of text2.matchAll(re2)) {
10679
10691
  const left = m[1] ?? "";
10680
10692
  const right = m[2] ?? "";
10681
10693
  if (/\d/.test(left) || /\d/.test(right)) continue;
10682
- count++;
10694
+ count2++;
10683
10695
  if (!first) first = m[0] ?? "";
10684
10696
  }
10685
10697
  }
10686
- return { count, first };
10698
+ return { count: count2, first };
10687
10699
  }
10688
10700
  function countStaccato(text2) {
10689
10701
  const sentences = text2.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
10690
10702
  let run = 0;
10691
- let count = 0;
10703
+ let count2 = 0;
10692
10704
  let first = "";
10693
10705
  let runStart = 0;
10694
10706
  for (const [i, s] of sentences.entries()) {
@@ -10697,17 +10709,17 @@ function countStaccato(text2) {
10697
10709
  if (run === 0) runStart = i;
10698
10710
  run++;
10699
10711
  if (run === STACCATO_RUN) {
10700
- count++;
10712
+ count2++;
10701
10713
  if (!first) first = sentences.slice(runStart, i + 1).join(" ");
10702
10714
  }
10703
10715
  } else {
10704
10716
  run = 0;
10705
10717
  }
10706
10718
  }
10707
- return { count, first };
10719
+ return { count: count2, first };
10708
10720
  }
10709
10721
  function countTitleCaseHeadings(text2) {
10710
- let count = 0;
10722
+ let count2 = 0;
10711
10723
  let first = "";
10712
10724
  for (const m of text2.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
10713
10725
  const heading = (m[1] ?? "").trim();
@@ -10715,10 +10727,10 @@ function countTitleCaseHeadings(text2) {
10715
10727
  if (words2.length < 4) continue;
10716
10728
  const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
10717
10729
  if (capitalized / words2.length < 0.8) continue;
10718
- count++;
10730
+ count2++;
10719
10731
  if (!first) first = heading;
10720
10732
  }
10721
- return { count, first };
10733
+ return { count: count2, first };
10722
10734
  }
10723
10735
  function countInlineHeaderList(text2) {
10724
10736
  const matches = [...text2.matchAll(/^\s*(?:[-*+]|\d+\.)\s+\*\*[^*\n]{2,40}:?\*\*:?\s/gm)];
@@ -10728,15 +10740,15 @@ function countInlineHeaderList(text2) {
10728
10740
  var EMOJI = /[\u{1F300}-\u{1FAFF}]|[\u{2600}-\u{27BF}]|[\u{1F000}-\u{1F2FF}]|\u{FE0F}/gu;
10729
10741
  var CURLY = /[‘’“”]/g;
10730
10742
  function countPhraseTell(text2, id, phrases) {
10731
- let count = 0;
10743
+ let count2 = 0;
10732
10744
  let first = "";
10733
10745
  for (const phrase of phrases) {
10734
10746
  for (const m of text2.matchAll(matcherFor(id, phrase))) {
10735
- count++;
10747
+ count2++;
10736
10748
  if (!first) first = m[0] ?? "";
10737
10749
  }
10738
10750
  }
10739
- return { count, first };
10751
+ return { count: count2, first };
10740
10752
  }
10741
10753
  function countPattern(text2, re2) {
10742
10754
  const matches = text2.match(re2) ?? [];
@@ -10974,7 +10986,7 @@ function renderOutcome(data, lines) {
10974
10986
  const status = op.result?.status ?? "pending";
10975
10987
  counts.set(status, (counts.get(status) ?? 0) + 1);
10976
10988
  }
10977
- const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
10989
+ const summary = [...counts].map(([status, count2]) => `${count2} ${status}`).join(", ");
10978
10990
  lines.push(`Google Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
10979
10991
  for (const op of data.ops) {
10980
10992
  const status = op.result?.status;
@@ -11976,11 +11988,11 @@ function renderKeywordLimits(limits) {
11976
11988
  const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
11977
11989
  (reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
11978
11990
  );
11979
- const count = limits.keywords.size;
11991
+ const count2 = limits.keywords.size;
11980
11992
  return [
11981
11993
  {
11982
11994
  code: "KEYWORD_SERVING_LIMITED",
11983
- message: `${count} ${count === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
11995
+ message: `${count2} ${count2 === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
11984
11996
  }
11985
11997
  ];
11986
11998
  }
@@ -16678,11 +16690,11 @@ function nestAddressFlags(row) {
16678
16690
  }
16679
16691
  function readNumericFlags(args4, row) {
16680
16692
  if (args4["conversion-count"] !== void 0 && args4["conversion-count"] !== "") {
16681
- const count = Number(args4["conversion-count"]);
16682
- if (!Number.isInteger(count) || count < 1) {
16693
+ const count2 = Number(args4["conversion-count"]);
16694
+ if (!Number.isInteger(count2) || count2 < 1) {
16683
16695
  failWriteValidation("--conversion-count must be a whole number of conversions, e.g. 3");
16684
16696
  }
16685
- row.conversionCount = count;
16697
+ row.conversionCount = count2;
16686
16698
  }
16687
16699
  if (args4.value !== void 0 && args4.value !== "") {
16688
16700
  const numeric = Number(args4.value);
@@ -17243,12 +17255,12 @@ function keywordKillHints(status) {
17243
17255
  const kills = typeof status === "string" && ["PAUSED", "REMOVED"].includes(status.toUpperCase());
17244
17256
  return kills ? KEYWORD_KILL_HINTS : [];
17245
17257
  }
17246
- function keywordFinalUrlHints(finalUrl, count) {
17258
+ function keywordFinalUrlHints(finalUrl, count2) {
17247
17259
  if (typeof finalUrl !== "string" || finalUrl.length === 0) {
17248
17260
  return [];
17249
17261
  }
17250
- return count > 1 ? [
17251
- `--final-url applied the same landing page to all ${count} keywords \u2014 the flag has no per-keyword form. A keyword-level URL only earns its place when that keyword must land somewhere the ad doesn't, so one URL shared by every keyword belongs on the ad instead: set the ad's final URL to this page and re-stage the keywords without --final-url. Keep it only if this page is genuinely each keyword's own destination and not the ad's.`
17262
+ return count2 > 1 ? [
17263
+ `--final-url applied the same landing page to all ${count2} keywords \u2014 the flag has no per-keyword form. A keyword-level URL only earns its place when that keyword must land somewhere the ad doesn't, so one URL shared by every keyword belongs on the ad instead: set the ad's final URL to this page and re-stage the keywords without --final-url. Keep it only if this page is genuinely each keyword's own destination and not the ad's.`
17252
17264
  ] : [
17253
17265
  "Keep --final-url only if this keyword's destination differs from the ad's final URL \u2014 otherwise drop it and let the keyword inherit the ad's landing page."
17254
17266
  ];
@@ -21849,7 +21861,7 @@ function renderOutcome2(data, lines) {
21849
21861
  const status = op.result?.status ?? "pending";
21850
21862
  counts.set(status, (counts.get(status) ?? 0) + 1);
21851
21863
  }
21852
- const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
21864
+ const summary = [...counts].map(([status, count2]) => `${count2} ${status}`).join(", ");
21853
21865
  lines.push(`LinkedIn Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
21854
21866
  for (const op of data.ops ?? []) {
21855
21867
  const status = op.result?.status;
@@ -26986,12 +26998,12 @@ function compact(payload) {
26986
26998
  return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== void 0));
26987
26999
  }
26988
27000
  function batchNudge2(data) {
26989
- const count = data?.sameKindStaged;
26990
- if (!count) {
27001
+ const count2 = data?.sameKindStaged;
27002
+ if (!count2) {
26991
27003
  return [];
26992
27004
  }
26993
27005
  return [
26994
- `${count} other change${count === 1 ? "" : "s"} of this kind and payload ${count === 1 ? "is" : "are"} already staged \u2014 pass every id to one call (\`--ids a,b,c\`) instead of one call each.`
27006
+ `${count2} other change${count2 === 1 ? "" : "s"} of this kind and payload ${count2 === 1 ? "is" : "are"} already staged \u2014 pass every id to one call (\`--ids a,b,c\`) instead of one call each.`
26995
27007
  ];
26996
27008
  }
26997
27009
  async function stageXOp(raw, hints2) {
@@ -35848,14 +35860,14 @@ function canvasDims(ar) {
35848
35860
  function fillPanel(label, w, h, out) {
35849
35861
  return `[${label}]scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h},setsar=1,fps=30[${out}]`;
35850
35862
  }
35851
- function splitStackArgs(count, axis, dims) {
35863
+ function splitStackArgs(count2, axis, dims) {
35852
35864
  const span = axis === "horizontal" ? dims.w : dims.h;
35853
- const base = Math.floor(span / count / 2) * 2;
35854
- const sizeAt = (i) => i === count - 1 ? span - base * (count - 1) : base;
35865
+ const base = Math.floor(span / count2 / 2) * 2;
35866
+ const sizeAt = (i) => i === count2 - 1 ? span - base * (count2 - 1) : base;
35855
35867
  const inputs = [];
35856
35868
  const filt = [];
35857
35869
  let labels = "";
35858
- for (let i = 0; i < count; i++) {
35870
+ for (let i = 0; i < count2; i++) {
35859
35871
  inputs.push("-i", `{{in.c${i}}}`);
35860
35872
  const pw = axis === "horizontal" ? sizeAt(i) : dims.w;
35861
35873
  const ph = axis === "vertical" ? sizeAt(i) : dims.h;
@@ -35863,7 +35875,7 @@ function splitStackArgs(count, axis, dims) {
35863
35875
  labels += `[p${i}]`;
35864
35876
  }
35865
35877
  const stack = axis === "vertical" ? "vstack" : "hstack";
35866
- filt.push(`${labels}${stack}=inputs=${count}[v]`);
35878
+ filt.push(`${labels}${stack}=inputs=${count2}[v]`);
35867
35879
  return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
35868
35880
  }
35869
35881
  function overlayXY(position, marginPx) {
@@ -42537,13 +42549,13 @@ async function markDirectoryIsReal(dir) {
42537
42549
  var INK_ALPHA = 128;
42538
42550
  function inkLuma(rgba) {
42539
42551
  let sum = 0;
42540
- let count = 0;
42552
+ let count2 = 0;
42541
42553
  for (let i = 0; i + 3 < rgba.length; i += 4) {
42542
42554
  if (rgba[i + 3] < INK_ALPHA) continue;
42543
42555
  sum += 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
42544
- count += 1;
42556
+ count2 += 1;
42545
42557
  }
42546
- return count === 0 ? null : sum / count;
42558
+ return count2 === 0 ? null : sum / count2;
42547
42559
  }
42548
42560
  var LIGHT_INK_LUMA = 170;
42549
42561
  async function markInkIsLight(markPath) {
@@ -45008,185 +45020,6 @@ async function foldExperiment(root, target) {
45008
45020
  };
45009
45021
  }
45010
45022
 
45011
- // src/commands/experiment/hints.ts
45012
- var LIVE_HINT = {
45013
- keep_running: (row) => `${row.experimentId} cannot yet tell the two pages apart \u2014 do not read its numbers as a result, and do not end it hoping for one. ${describeWait(row)}`,
45014
- winner: (row) => row.winner === "variant" ? `${row.experimentId} has a winner: the new variant. Run \`baker experiment finish --id ${row.experimentId} --keep variant\` to show it to everyone, then \`baker experiment fold\`.` : `${row.experimentId} has a winner: the page as it is. Run \`baker experiment finish --id ${row.experimentId} --keep original\` to put all the traffic back on it.`,
45015
- no_difference: (row) => `${row.experimentId} measured the two pages as alike \u2014 it collected enough traffic to see the change it was looking for, and there was none. End it with \`--keep original\` and test a bigger change \u2014 a headline, an offer, the shape of the page, not a button colour.`,
45016
- stopped_early_harmful: (row) => `${row.experimentId} is doing damage and is STILL being shown to visitors. Run \`baker experiment finish --id ${row.experimentId} --keep original\` now.`,
45017
- // Unreachable on a live test — only ending one produces it. The Record is
45018
- // exhaustive by type so a verdict added upstream fails to compile here rather
45019
- // than printing nothing.
45020
- inconclusive: (row) => `${row.experimentId} was ended before it concluded and found nothing.`,
45021
- invalid: (row) => `${row.experimentId} cannot be read: ${row.invalidReason === "crossover" ? "too many visitors saw both versions" : "the traffic did not split evenly"}. End it with \`--keep original\` and start again rather than reporting anything from it.`
45022
- };
45023
- function describeWait(row) {
45024
- if (row.willNotConclude) {
45025
- return "This page's traffic will not settle it at any point, so end it with `--keep original` and test a bigger change.";
45026
- }
45027
- if (row.earliestDecisionAt === null) {
45028
- return "This page has produced no visitors yet, so there is nothing to project a finish date from.";
45029
- }
45030
- return `Earliest it could say anything: ${new Date(row.earliestDecisionAt).toISOString().slice(0, 10)}.`;
45031
- }
45032
- function pausedNote(row) {
45033
- return row.status === "paused" ? `${row.experimentId} is paused \u2014 everyone sees the original. \`baker experiment resume --id ${row.experimentId}\` restarts the split with the same sides. ` : "";
45034
- }
45035
- function unlearnedHint(experiments) {
45036
- const unlearned = experiments.filter(
45037
- (row) => row.status === "finished" && row.learning === null && row.verdict !== "inconclusive"
45038
- );
45039
- if (unlearned.length === 0) return null;
45040
- const ids = unlearned.map((row) => row.experimentId).join(", ");
45041
- return `${unlearned.length === 1 ? "One finished test has" : `${unlearned.length} finished tests have`} no learning written down yet (${ids}). Say what it taught in one or two sentences \u2014 what the client should believe about this page now \u2014 with \`baker experiment update --id <id> --learning "\u2026"\`. It is what \`plan\` hands the next test.`;
45042
- }
45043
- function buildStatusHints(experiments) {
45044
- const hints2 = experiments.filter((row) => row.status !== "finished").map((row) => pausedNote(row) + LIVE_HINT[row.verdict](row));
45045
- const unlearned = unlearnedHint(experiments);
45046
- if (unlearned) hints2.push(unlearned);
45047
- if (experiments.length === 0) {
45048
- hints2.push(
45049
- "No tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page has enough traffic for one before you build the alternative."
45050
- );
45051
- }
45052
- return hints2;
45053
- }
45054
- function asLift(value) {
45055
- return `+${Math.round(value * 100)}%`;
45056
- }
45057
- function horizonsHint(plan) {
45058
- const settled = plan.horizons.filter((horizon) => horizon.detectableLift !== null);
45059
- if (settled.length === 0) {
45060
- return "At this page's traffic no window up to 90 days would gather enough visitors to settle anything, at any size of change. The constraint is the traffic, not the test.";
45061
- }
45062
- const parts = settled.map((horizon) => `${horizon.days} days \u2192 ${asLift(horizon.detectableLift ?? 0)}`);
45063
- return `The smallest change each window could settle on this page's traffic: ${parts.join(", ")}. Design the alternative to be at least that different.`;
45064
- }
45065
- function conversionsHint(plan) {
45066
- if (plan.conversions.length <= 1) return null;
45067
- const others = plan.conversions.filter((entry) => entry.eventKey !== plan.goal);
45068
- return `This page's visitors also went on to: ${others.map((entry) => `\`${entry.eventKey}\`${entry.name ? ` (\u201C${entry.name}\u201D)` : ""} \u2014 ${entry.convertingVisitors}`).join(
45069
- ", "
45070
- )}. Any of them can be the goal (--goal <key>); the named ones are reported beside it on every result, and decide nothing.`;
45071
- }
45072
- function pastTestLine(test) {
45073
- const outcome = {
45074
- supported: "the idea held",
45075
- refuted_opposite: "the idea was WRONG \u2014 the change made it worse",
45076
- refuted_no_effect: "no difference \u2014 the change did not matter",
45077
- unanswered: "never answered"
45078
- };
45079
- const kept = test.survivor === "variant" ? "the new version was kept" : "the original was kept";
45080
- const learning = test.learning ? ` Learning: ${test.learning}` : "";
45081
- return `\u201C${test.hypothesis.change}\u201D (${test.experimentId}, ${test.ranForDays} days): ${outcome[test.hypothesisOutcome]}; ${kept}${test.againstVerdict ? " against the numbers" : ""}.${learning}`;
45082
- }
45083
- function pastTestsHint(plan) {
45084
- if (plan.pastTests.length === 0) return null;
45085
- const lines = plan.pastTests.slice(0, 5).map(pastTestLine);
45086
- const more = plan.pastTests.length > 5 ? ` \u2026and ${plan.pastTests.length - 5} more \u2014 \`baker experiment history --landing <slug>\` lists them all.` : "";
45087
- return `This page has been tested before \u2014 read this before choosing what to change, and build on it rather than repeating it: ${lines.join(" ")}${more}`;
45088
- }
45089
- function buildPlanHints(plan) {
45090
- const horizons = horizonsHint(plan);
45091
- const past = pastTestsHint(plan);
45092
- if (!plan.canRun) {
45093
- return [
45094
- plan.reason ?? "This test cannot run on this page.",
45095
- ...horizons && plan.visitors > 0 ? [horizons] : [],
45096
- ...past ? [past] : [],
45097
- "A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
45098
- ];
45099
- }
45100
- const hints2 = [
45101
- ...past ? [past] : [],
45102
- // First, because it changes how every number below should be read: a plan
45103
- // sized on a stand-in is a guide, and the agent must say so to the client
45104
- // rather than quote "about 70 days" as though it were measured.
45105
- ...plan.warning ? [plan.warning] : [],
45106
- `This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
45107
- // Fires at the exact moment the hypothesis gets invented, whether or not
45108
- // the agent read the family doc. `plan` says a question CAN be settled here
45109
- // and says nothing about which question is worth asking — and a well-run
45110
- // test of a bad idea spends the page's traffic and returns `no_difference`.
45111
- "Now decide WHAT to change, from evidence rather than taste. These numbers say which page and which step; they cannot say why. A second opinion answers why \u2014 Microsoft Clarity for what visitors actually do on the page, `baker gsc` for what they searched, `baker ads <platform>` for what the ad promised, `baker ga4` for the client's own measurement. `baker capabilities` lists everything this client has in one call. Message match \u2014 what the ad or the search promised against what the page opens with \u2014 is the cheapest hypothesis worth testing and is wrong surprisingly often.",
45112
- // The base always works, so a missing integration is never a reason to
45113
- // stop. Naming what it would have shown is worth more to the client than
45114
- // either a silent gap or a run that waited.
45115
- "If none of that is connected, this page's own drop-off is enough to test on \u2014 say which source you lacked and what it would have told you, then run the test anyway. A test never waits on an integration."
45116
- ];
45117
- const conversions = conversionsHint(plan);
45118
- if (conversions) hints2.push(conversions);
45119
- if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
45120
- hints2.push(
45121
- `At this page's recent traffic it would take about ${plan.estimatedDays} days \u2014 a guide, not a limit: it ends when the numbers are clear, and more traffic (a campaign, ads) shortens it. If the traffic will stay as it is, test a bigger change, which needs fewer visitors to detect.`
45122
- );
45123
- }
45124
- if (horizons) hints2.push(horizons);
45125
- return hints2;
45126
- }
45127
- function buildHistoryHints(tests, landing) {
45128
- if (tests.length === 0) {
45129
- return [
45130
- landing ? `No finished test on \u201C${landing}\u201D yet \u2014 nothing to build on, and nothing to avoid repeating. \`baker experiment plan\` says whether the page can settle one.` : "No finished tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page can settle one."
45131
- ];
45132
- }
45133
- const hints2 = [];
45134
- const supported = tests.filter((test) => test.hypothesisOutcome === "supported");
45135
- const refuted = tests.filter((test) => test.hypothesisOutcome === "refuted_opposite");
45136
- const noEffect = tests.filter((test) => test.hypothesisOutcome === "refuted_no_effect");
45137
- if (supported.length > 0) {
45138
- hints2.push(
45139
- `Held: ${supported.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Build on these \u2014 the same insight usually has more in it.`
45140
- );
45141
- }
45142
- if (refuted.length > 0) {
45143
- hints2.push(
45144
- `Made it worse: ${refuted.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Do not propose these again as they were; the observation behind each may still be right, the change was not.`
45145
- );
45146
- }
45147
- if (noEffect.length > 0) {
45148
- hints2.push(
45149
- `Did not matter: ${noEffect.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. The next change on these pages has to be bigger \u2014 a headline, an offer, the shape of the page.`
45150
- );
45151
- }
45152
- const unlearned = tests.filter((test) => test.learning === null && test.verdict !== "inconclusive");
45153
- if (unlearned.length > 0) {
45154
- hints2.push(
45155
- `${unlearned.length === 1 ? "One test has" : `${unlearned.length} tests have`} no learning written down (${unlearned.map((test) => test.experimentId).join(", ")}). Read its numbers and write one: \`baker experiment update --id <id> --learning "\u2026"\`.`
45156
- );
45157
- }
45158
- return hints2;
45159
- }
45160
- function buildStartHints(staged) {
45161
- const hints2 = [];
45162
- for (const entry of staged.drift) {
45163
- hints2.push(driftHint(staged, entry));
45164
- }
45165
- if (staged.composition && staged.composition.forked.length === 0) {
45166
- hints2.push(
45167
- `\u201C${staged.variant}\u201D does not have its own copy of any section \u2014 it renders exactly the same page as \u201C${staged.landing}\u201D, so this test cannot find anything. Fork the section you want to test (\`baker landing variant ${staged.landing} --fork Hero.astro\`) and edit only that file.`
45168
- );
45169
- }
45170
- if (staged.newForms.length > 0 && staged.variantGoal === void 0) {
45171
- const forms = staged.newForms.map((flow) => `\u201C${flow}\u201D`).join(", ");
45172
- hints2.push(
45173
- `\u201C${staged.variant}\u201D uses ${staged.newForms.length === 1 ? "a Form" : "Forms"} the page does not: ${forms}. The test reads the new version on \`${staged.goal}\`, the page's event \u2014 if the new version converts by reaching this Form's ending instead, it can never convert on that key and the test will call it harmful. Say its own event: \`baker experiment update --id <id> --variant-goal submit:${staged.newForms[0]}\` (or the confirmation step's \`form:${staged.newForms[0]}:<node>:<trigger>\` key, from \`baker analytics conversions --candidates\` once the Form has been opened in the preview).`
45174
- );
45175
- }
45176
- return hints2;
45177
- }
45178
- function driftHint(staged, entry) {
45179
- const suffix = ` Check both versions before reading that number, and give the new version's step its own name if it is not the same action (\`baker analytics conversions --candidates\`).`;
45180
- if (entry.reason === "trigger_differs") {
45181
- return `\u201C${staged.landing}\u201D and \u201C${staged.variant}\u201D both count \`${entry.name}\`, but one fires it when the element is **scrolled into view** and the other when the visitor **does something**. Those are two different questions with one answer, and the test will report a difference that is the trigger rather than the page.${suffix}`;
45182
- }
45183
- if (entry.reason === "variant_only_missing" || entry.reason === "control_only_missing") {
45184
- const missing = entry.reason === "variant_only_missing" ? staged.variant : staged.landing;
45185
- return `Only one version fires \`${entry.name}\` \u2014 \u201C${missing}\u201D does not declare it at all, so it can only ever read zero for that conversion and the comparison will look like a collapse.${suffix}`;
45186
- }
45187
- return `Both versions count \`${entry.name}\`, and the section the test changed is what fires it \u2014 the markup around it is not the same on the two pages. If the new version fires it on a different action (a block that is visible from the start rather than one the visitor opens), that conversion is comparing two different things.${suffix}`;
45188
- }
45189
-
45190
45023
  // ../api/src/experiments/composition.ts
45191
45024
  import { z as z41 } from "zod";
45192
45025
  var experimentCompositionSchema = z41.object({
@@ -45251,6 +45084,25 @@ var experimentHypothesisOutcomeSchema = z43.enum([
45251
45084
  "unanswered"
45252
45085
  ]);
45253
45086
 
45087
+ // ../api/src/experiments/wait.ts
45088
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
45089
+ function count(value, unit) {
45090
+ return `about ${value} ${unit}${value === 1 ? "" : "s"}`;
45091
+ }
45092
+ function describeWait(earliestDecisionAt, now) {
45093
+ const days = (earliestDecisionAt - now) / DAY_MS5;
45094
+ if (days <= 0) return "any moment now";
45095
+ if (days < 1) return "within a day";
45096
+ if (days < 14) return count(Math.round(days), "day");
45097
+ if (days < 60) return count(Math.round(days / 7), "week");
45098
+ return count(Math.round(days / 30), "month");
45099
+ }
45100
+ function roughCount(count2) {
45101
+ if (count2 < 10) return `about ${Math.max(1, Math.round(count2)).toLocaleString("en-US")}`;
45102
+ const magnitude = 10 ** Math.max(1, Math.floor(Math.log10(count2)) - 1);
45103
+ return `about ${(Math.round(count2 / magnitude) * magnitude).toLocaleString("en-US")}`;
45104
+ }
45105
+
45254
45106
  // ../api/src/experiments/wire.ts
45255
45107
  import { z as z44 } from "zod";
45256
45108
  var slugSchema = z44.string().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a page slug, like `oferta`");
@@ -45470,7 +45322,17 @@ var experimentVerdictSchema = z44.enum([
45470
45322
  "keep_running",
45471
45323
  "winner",
45472
45324
  "no_difference",
45325
+ /**
45326
+ * Even the best case is under the lift the test was built to find, so there
45327
+ * is nothing left for it to discover: keep the original. The "is this a
45328
+ * failure" answer — not proven worse, but proven not worth running.
45329
+ */
45330
+ "cannot_win",
45473
45331
  "invalid",
45332
+ /**
45333
+ * Only on tests read before the stopping rule was one interval: a version
45334
+ * proven worse is now `winner` with `winner: "control"`, whenever it is seen.
45335
+ */
45474
45336
  "stopped_early_harmful",
45475
45337
  /**
45476
45338
  * Ended before the numbers could say anything. Never produced by the
@@ -45615,6 +45477,14 @@ var experimentStatusRowSchema = z44.object({
45615
45477
  precision: z44.number(),
45616
45478
  /** When the test could first say something. `null` with no traffic to project from. */
45617
45479
  earliestDecisionAt: z44.number().nullable(),
45480
+ /**
45481
+ * What the test is still missing before its verdict changes, if the gap it
45482
+ * is seeing holds: visitors across both arms, and the conversions they bring.
45483
+ * A test is gated on data, never on days — `earliestDecisionAt` is only this
45484
+ * divided by the page's pace. Null once it has spoken, before anyone has
45485
+ * arrived, or when no amount of traffic would settle it.
45486
+ */
45487
+ stillNeeded: z44.object({ visitors: z44.number(), conversions: z44.number() }).nullable().default(null),
45618
45488
  /**
45619
45489
  * True when no amount of this page's traffic would settle the comparison.
45620
45490
  * A different thing from a page that is merely slow, and the reason to end
@@ -45699,6 +45569,190 @@ var experimentFinishResponseSchema = z44.object({
45699
45569
  summary: z44.string()
45700
45570
  });
45701
45571
 
45572
+ // src/commands/experiment/hints.ts
45573
+ var LIVE_HINT = {
45574
+ keep_running: (row, now) => `${row.experimentId} cannot yet tell the two pages apart \u2014 do not end it hoping for a result. ${describeWait2(row, now)} To the user, say only this in plain words: "${row.summary}" plus how much more data it needs and roughly when. Never quote confidence, chance-better or intervals to them \u2014 those numbers confuse marketers and decide nothing.`,
45575
+ winner: (row) => row.winner === "variant" ? `${row.experimentId} has a winner: the new variant. Run \`baker experiment finish --id ${row.experimentId} --keep variant\` to show it to everyone, then \`baker experiment fold\`.` : `${row.experimentId}: the new version is proven worse and is STILL being shown to half the visitors. Run \`baker experiment finish --id ${row.experimentId} --keep original\` now.`,
45576
+ no_difference: (row) => `${row.experimentId} measured the two pages as alike \u2014 it collected enough traffic to see the change it was looking for, and there was none. End it with \`--keep original\` and test a bigger change \u2014 a headline, an offer, the shape of the page, not a button colour.`,
45577
+ cannot_win: (row) => `${row.experimentId}: the new version cannot win \u2014 even its best case is under the lift this test was looking for, so there is nothing left to find. Run \`baker experiment finish --id ${row.experimentId} --keep original --reason numbers_clear\` and write down what it taught.`,
45578
+ stopped_early_harmful: (row) => `${row.experimentId} is doing damage and is STILL being shown to visitors. Run \`baker experiment finish --id ${row.experimentId} --keep original\` now.`,
45579
+ // Unreachable on a live test — only ending one produces it. The Record is
45580
+ // exhaustive by type so a verdict added upstream fails to compile here rather
45581
+ // than printing nothing.
45582
+ inconclusive: (row) => `${row.experimentId} was ended before it concluded and found nothing.`,
45583
+ invalid: (row) => `${row.experimentId} cannot be read: ${row.invalidReason === "crossover" ? "too many visitors saw both versions" : "the traffic did not split evenly"}. End it with \`--keep original\` and start again rather than reporting anything from it.`
45584
+ };
45585
+ function describeWait2(row, now) {
45586
+ if (row.willNotConclude) {
45587
+ return "This page's traffic will not settle it at any point, so end it with `--keep original` and test a bigger change.";
45588
+ }
45589
+ if (row.earliestDecisionAt === null) {
45590
+ return "This page has produced no visitors yet, so there is nothing to project a finish date from.";
45591
+ }
45592
+ if (row.stillNeeded === null) {
45593
+ return `It already has the data it needs; the verdict waits only until it has seen a full day of visitors \u2014 ${describeWait(row.earliestDecisionAt, now)}.`;
45594
+ }
45595
+ const needed = `It needs ${roughCount(row.stillNeeded.visitors)} more visitors (${roughCount(row.stillNeeded.conversions)} more conversions) for an answer if the gap holds \u2014 `;
45596
+ return `${needed}${describeWait(row.earliestDecisionAt, now)} at today's traffic (${new Date(row.earliestDecisionAt).toISOString().slice(0, 10)}). A test is gated on this data, never on days.`;
45597
+ }
45598
+ function pausedNote(row) {
45599
+ return row.status === "paused" ? `${row.experimentId} is paused \u2014 everyone sees the original. \`baker experiment resume --id ${row.experimentId}\` restarts the split with the same sides. ` : "";
45600
+ }
45601
+ function unlearnedHint(experiments) {
45602
+ const unlearned = experiments.filter(
45603
+ (row) => row.status === "finished" && row.learning === null && row.verdict !== "inconclusive"
45604
+ );
45605
+ if (unlearned.length === 0) return null;
45606
+ const ids = unlearned.map((row) => row.experimentId).join(", ");
45607
+ return `${unlearned.length === 1 ? "One finished test has" : `${unlearned.length} finished tests have`} no learning written down yet (${ids}). Say what it taught in one or two sentences \u2014 what the client should believe about this page now \u2014 with \`baker experiment update --id <id> --learning "\u2026"\`. It is what \`plan\` hands the next test.`;
45608
+ }
45609
+ function buildStatusHints(experiments, now = Date.now()) {
45610
+ const hints2 = experiments.filter((row) => row.status !== "finished").map((row) => pausedNote(row) + LIVE_HINT[row.verdict](row, now));
45611
+ const unlearned = unlearnedHint(experiments);
45612
+ if (unlearned) hints2.push(unlearned);
45613
+ if (experiments.length === 0) {
45614
+ hints2.push(
45615
+ "No tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page has enough traffic for one before you build the alternative."
45616
+ );
45617
+ }
45618
+ return hints2;
45619
+ }
45620
+ function asLift(value) {
45621
+ return `+${Math.round(value * 100)}%`;
45622
+ }
45623
+ function horizonsHint(plan) {
45624
+ const settled = plan.horizons.filter((horizon) => horizon.detectableLift !== null);
45625
+ if (settled.length === 0) {
45626
+ return "At this page's traffic no window up to 90 days would gather enough visitors to settle anything, at any size of change. The constraint is the traffic, not the test.";
45627
+ }
45628
+ const parts = settled.map((horizon) => `${horizon.days} days \u2192 ${asLift(horizon.detectableLift ?? 0)}`);
45629
+ return `The smallest change each window could settle on this page's traffic: ${parts.join(", ")}. Design the alternative to be at least that different.`;
45630
+ }
45631
+ function conversionsHint(plan) {
45632
+ if (plan.conversions.length <= 1) return null;
45633
+ const others = plan.conversions.filter((entry) => entry.eventKey !== plan.goal);
45634
+ return `This page's visitors also went on to: ${others.map((entry) => `\`${entry.eventKey}\`${entry.name ? ` (\u201C${entry.name}\u201D)` : ""} \u2014 ${entry.convertingVisitors}`).join(
45635
+ ", "
45636
+ )}. Any of them can be the goal (--goal <key>); the named ones are reported beside it on every result, and decide nothing.`;
45637
+ }
45638
+ function pastTestLine(test) {
45639
+ const outcome = {
45640
+ supported: "the idea held",
45641
+ refuted_opposite: "the idea was WRONG \u2014 the change made it worse",
45642
+ refuted_no_effect: "no difference \u2014 the change did not matter",
45643
+ unanswered: "never answered"
45644
+ };
45645
+ const kept = test.survivor === "variant" ? "the new version was kept" : "the original was kept";
45646
+ const learning = test.learning ? ` Learning: ${test.learning}` : "";
45647
+ return `\u201C${test.hypothesis.change}\u201D (${test.experimentId}, ${test.ranForDays} days): ${outcome[test.hypothesisOutcome]}; ${kept}${test.againstVerdict ? " against the numbers" : ""}.${learning}`;
45648
+ }
45649
+ function pastTestsHint(plan) {
45650
+ if (plan.pastTests.length === 0) return null;
45651
+ const lines = plan.pastTests.slice(0, 5).map(pastTestLine);
45652
+ const more = plan.pastTests.length > 5 ? ` \u2026and ${plan.pastTests.length - 5} more \u2014 \`baker experiment history --landing <slug>\` lists them all.` : "";
45653
+ return `This page has been tested before \u2014 read this before choosing what to change, and build on it rather than repeating it: ${lines.join(" ")}${more}`;
45654
+ }
45655
+ function buildPlanHints(plan) {
45656
+ const horizons = horizonsHint(plan);
45657
+ const past = pastTestsHint(plan);
45658
+ if (!plan.canRun) {
45659
+ return [
45660
+ plan.reason ?? "This test cannot run on this page.",
45661
+ ...horizons && plan.visitors > 0 ? [horizons] : [],
45662
+ ...past ? [past] : [],
45663
+ "A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
45664
+ ];
45665
+ }
45666
+ const hints2 = [
45667
+ ...past ? [past] : [],
45668
+ // First, because it changes how every number below should be read: a plan
45669
+ // sized on a stand-in is a guide, and the agent must say so to the client
45670
+ // rather than quote "about 70 days" as though it were measured.
45671
+ ...plan.warning ? [plan.warning] : [],
45672
+ `This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
45673
+ // Fires at the exact moment the hypothesis gets invented, whether or not
45674
+ // the agent read the family doc. `plan` says a question CAN be settled here
45675
+ // and says nothing about which question is worth asking — and a well-run
45676
+ // test of a bad idea spends the page's traffic and returns `no_difference`.
45677
+ "Now decide WHAT to change, from evidence rather than taste. These numbers say which page and which step; they cannot say why. A second opinion answers why \u2014 Microsoft Clarity for what visitors actually do on the page, `baker gsc` for what they searched, `baker ads <platform>` for what the ad promised, `baker ga4` for the client's own measurement. `baker capabilities` lists everything this client has in one call. Message match \u2014 what the ad or the search promised against what the page opens with \u2014 is the cheapest hypothesis worth testing and is wrong surprisingly often.",
45678
+ // The base always works, so a missing integration is never a reason to
45679
+ // stop. Naming what it would have shown is worth more to the client than
45680
+ // either a silent gap or a run that waited.
45681
+ "If none of that is connected, this page's own drop-off is enough to test on \u2014 say which source you lacked and what it would have told you, then run the test anyway. A test never waits on an integration."
45682
+ ];
45683
+ const conversions = conversionsHint(plan);
45684
+ if (conversions) hints2.push(conversions);
45685
+ if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
45686
+ hints2.push(
45687
+ `At this page's recent traffic it would take about ${plan.estimatedDays} days \u2014 a guide, not a limit: it ends when the numbers are clear, and more traffic (a campaign, ads) shortens it. If the traffic will stay as it is, test a bigger change, which needs fewer visitors to detect.`
45688
+ );
45689
+ }
45690
+ if (horizons) hints2.push(horizons);
45691
+ return hints2;
45692
+ }
45693
+ function buildHistoryHints(tests, landing) {
45694
+ if (tests.length === 0) {
45695
+ return [
45696
+ landing ? `No finished test on \u201C${landing}\u201D yet \u2014 nothing to build on, and nothing to avoid repeating. \`baker experiment plan\` says whether the page can settle one.` : "No finished tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page can settle one."
45697
+ ];
45698
+ }
45699
+ const hints2 = [];
45700
+ const supported = tests.filter((test) => test.hypothesisOutcome === "supported");
45701
+ const refuted = tests.filter((test) => test.hypothesisOutcome === "refuted_opposite");
45702
+ const noEffect = tests.filter((test) => test.hypothesisOutcome === "refuted_no_effect");
45703
+ if (supported.length > 0) {
45704
+ hints2.push(
45705
+ `Held: ${supported.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Build on these \u2014 the same insight usually has more in it.`
45706
+ );
45707
+ }
45708
+ if (refuted.length > 0) {
45709
+ hints2.push(
45710
+ `Made it worse: ${refuted.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Do not propose these again as they were; the observation behind each may still be right, the change was not.`
45711
+ );
45712
+ }
45713
+ if (noEffect.length > 0) {
45714
+ hints2.push(
45715
+ `Did not matter: ${noEffect.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. The next change on these pages has to be bigger \u2014 a headline, an offer, the shape of the page.`
45716
+ );
45717
+ }
45718
+ const unlearned = tests.filter((test) => test.learning === null && test.verdict !== "inconclusive");
45719
+ if (unlearned.length > 0) {
45720
+ hints2.push(
45721
+ `${unlearned.length === 1 ? "One test has" : `${unlearned.length} tests have`} no learning written down (${unlearned.map((test) => test.experimentId).join(", ")}). Read its numbers and write one: \`baker experiment update --id <id> --learning "\u2026"\`.`
45722
+ );
45723
+ }
45724
+ return hints2;
45725
+ }
45726
+ function buildStartHints(staged) {
45727
+ const hints2 = [];
45728
+ for (const entry of staged.drift) {
45729
+ hints2.push(driftHint(staged, entry));
45730
+ }
45731
+ if (staged.composition && staged.composition.forked.length === 0) {
45732
+ hints2.push(
45733
+ `\u201C${staged.variant}\u201D does not have its own copy of any section \u2014 it renders exactly the same page as \u201C${staged.landing}\u201D, so this test cannot find anything. Fork the section you want to test (\`baker landing variant ${staged.landing} --fork Hero.astro\`) and edit only that file.`
45734
+ );
45735
+ }
45736
+ if (staged.newForms.length > 0 && staged.variantGoal === void 0) {
45737
+ const forms = staged.newForms.map((flow) => `\u201C${flow}\u201D`).join(", ");
45738
+ hints2.push(
45739
+ `\u201C${staged.variant}\u201D uses ${staged.newForms.length === 1 ? "a Form" : "Forms"} the page does not: ${forms}. The test reads the new version on \`${staged.goal}\`, the page's event \u2014 if the new version converts by reaching this Form's ending instead, it can never convert on that key and the test will call it harmful. Say its own event: \`baker experiment update --id <id> --variant-goal submit:${staged.newForms[0]}\` (or the confirmation step's \`form:${staged.newForms[0]}:<node>:<trigger>\` key, from \`baker analytics conversions --candidates\` once the Form has been opened in the preview).`
45740
+ );
45741
+ }
45742
+ return hints2;
45743
+ }
45744
+ function driftHint(staged, entry) {
45745
+ const suffix = ` Check both versions before reading that number, and give the new version's step its own name if it is not the same action (\`baker analytics conversions --candidates\`).`;
45746
+ if (entry.reason === "trigger_differs") {
45747
+ return `\u201C${staged.landing}\u201D and \u201C${staged.variant}\u201D both count \`${entry.name}\`, but one fires it when the element is **scrolled into view** and the other when the visitor **does something**. Those are two different questions with one answer, and the test will report a difference that is the trigger rather than the page.${suffix}`;
45748
+ }
45749
+ if (entry.reason === "variant_only_missing" || entry.reason === "control_only_missing") {
45750
+ const missing = entry.reason === "variant_only_missing" ? staged.variant : staged.landing;
45751
+ return `Only one version fires \`${entry.name}\` \u2014 \u201C${missing}\u201D does not declare it at all, so it can only ever read zero for that conversion and the comparison will look like a collapse.${suffix}`;
45752
+ }
45753
+ return `Both versions count \`${entry.name}\`, and the section the test changed is what fires it \u2014 the markup around it is not the same on the two pages. If the new version fires it on a different action (a block that is visible from the start rather than one the visitor opens), that conversion is comparing two different things.${suffix}`;
45754
+ }
45755
+
45702
45756
  // src/commands/experiment/hypothesis.ts
45703
45757
  var SOURCES = experimentEvidenceSourceSchema.options.join(", ");
45704
45758
  function parseOne(raw) {
@@ -46056,7 +46110,7 @@ registerSchema({
46056
46110
  var statusCommand6 = defineCommand123({
46057
46111
  meta: {
46058
46112
  name: "status",
46059
- description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | invalid | stopped_early_harmful. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
46113
+ description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | cannot_win | invalid. `cannot_win` means even the best case is under the lift the test was looking for \u2014 end it and keep the original. `stillNeeded` is the data (visitors, conversions) the test still needs; a test is gated on data, never on days. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
46060
46114
  },
46061
46115
  args: {
46062
46116
  id: { type: "string", description: "One test, by its id", required: false },
@@ -52809,7 +52863,7 @@ async function runMediaGroupLookup(input) {
52809
52863
  const data = await apiGet(`${input.path}?${query.toString()}`);
52810
52864
  const images = data.images.map((image) => compactImage(image));
52811
52865
  const videos = (data.videos ?? []).map((video) => compactVideo(video));
52812
- const count = images.length + videos.length;
52866
+ const count2 = images.length + videos.length;
52813
52867
  writeJson({
52814
52868
  ok: true,
52815
52869
  data: {
@@ -52819,11 +52873,11 @@ async function runMediaGroupLookup(input) {
52819
52873
  // place the campaign is named.
52820
52874
  groupTitle: data.groupTitle ?? null,
52821
52875
  groupUrl: data.groupUrl ?? null,
52822
- count,
52876
+ count: count2,
52823
52877
  images,
52824
52878
  videos
52825
52879
  },
52826
- hints: count > 1 ? ["These arrived together and are ordered. Read them in sequence before using any one of them alone."] : void 0
52880
+ hints: count2 > 1 ? ["These arrived together and are ordered. Read them in sequence before using any one of them alone."] : void 0
52827
52881
  });
52828
52882
  } catch (err) {
52829
52883
  if (err instanceof ApiError) {
@@ -53523,9 +53577,9 @@ function getDominantEdgeColor(data, width, height) {
53523
53577
  let maxCount = 0;
53524
53578
  let dominantColor = { r: 0, g: 0, b: 0 };
53525
53579
  for (const key in colorCount) {
53526
- const count = colorCount[key];
53527
- if (count > maxCount) {
53528
- maxCount = count;
53580
+ const count2 = colorCount[key];
53581
+ if (count2 > maxCount) {
53582
+ maxCount = count2;
53529
53583
  const parts = key.split(",").map(Number);
53530
53584
  dominantColor = { r: parts[0], g: parts[1], b: parts[2] };
53531
53585
  }
@@ -55969,10 +56023,10 @@ var ANALYZERS = [
55969
56023
  scope: "page",
55970
56024
  run: (text2, file) => {
55971
56025
  const body = stripHtmlToText(text2).replace(/&mdash;|&#0*8212;|&#x0*2014;/gi, "\u2014");
55972
- const count = (body.match(/[—]|(?<!-)--(?!-)(?=\S)/g) ?? []).length;
55973
- if (count < EM_DASH_FLOOR) return [];
55974
- if (body.length > count * EM_DASH_CHARS_PER_DASH) return [];
55975
- return [{ id: "em-dash-overuse", snippet: `${count} em-dashes in body copy`, file }];
56026
+ const count2 = (body.match(/[—]|(?<!-)--(?!-)(?=\S)/g) ?? []).length;
56027
+ if (count2 < EM_DASH_FLOOR) return [];
56028
+ if (body.length > count2 * EM_DASH_CHARS_PER_DASH) return [];
56029
+ return [{ id: "em-dash-overuse", snippet: `${count2} em-dashes in body copy`, file }];
55976
56030
  }
55977
56031
  },
55978
56032
  // Agent washing: an autonomy claim the page never scaffolds with a signal about
@@ -55999,25 +56053,25 @@ var ANALYZERS = [
55999
56053
  run: (text2, file) => {
56000
56054
  const body = stripHtmlToText(text2);
56001
56055
  const lower = body.toLowerCase();
56002
- let count = 0;
56056
+ let count2 = 0;
56003
56057
  let firstSample = "";
56004
56058
  for (const phrase of BUZZWORDS) {
56005
56059
  let from = 0;
56006
56060
  for (; ; ) {
56007
56061
  const idx = lower.indexOf(phrase, from);
56008
56062
  if (idx === -1) break;
56009
- count++;
56063
+ count2++;
56010
56064
  if (!firstSample) {
56011
56065
  firstSample = body.slice(Math.max(0, idx - 12), Math.min(body.length, idx + phrase.length + 12)).trim();
56012
56066
  }
56013
56067
  from = idx + phrase.length;
56014
56068
  }
56015
56069
  }
56016
- if (count === 0) return [];
56070
+ if (count2 === 0) return [];
56017
56071
  return [
56018
56072
  {
56019
56073
  id: "marketing-buzzword",
56020
- snippet: `${count} buzzword phrase${count === 1 ? "" : "s"}: "${firstSample}"`,
56074
+ snippet: `${count2} buzzword phrase${count2 === 1 ? "" : "s"}: "${firstSample}"`,
56021
56075
  file
56022
56076
  }
56023
56077
  ];
@@ -56029,18 +56083,18 @@ var ANALYZERS = [
56029
56083
  scope: "page",
56030
56084
  run: (text2, file) => {
56031
56085
  const body = stripHtmlToText(text2);
56032
- let count = 0;
56086
+ let count2 = 0;
56033
56087
  let firstSample = "";
56034
56088
  for (const m of body.matchAll(/\bNot an? [a-z][^.!?]{1,40}[.!]\s+[A-Z][^.!?]{1,60}[.!]/g)) {
56035
- count++;
56089
+ count2++;
56036
56090
  if (!firstSample) firstSample = cap2(m, 0).trim().slice(0, 80);
56037
56091
  }
56038
56092
  for (const m of body.matchAll(/\b[A-Z][^.!?]{4,80}[.!]\s+(No|Just)\s+[a-z][^.!?]{2,60}[.!]/g)) {
56039
- count++;
56093
+ count2++;
56040
56094
  if (!firstSample) firstSample = cap2(m, 0).trim().slice(0, 80);
56041
56095
  }
56042
- if (count < 3) return [];
56043
- return [{ id: "aphoristic-cadence", snippet: `${count} aphoristic constructions: "${firstSample}"`, file }];
56096
+ if (count2 < 3) return [];
56097
+ return [{ id: "aphoristic-cadence", snippet: `${count2} aphoristic constructions: "${firstSample}"`, file }];
56044
56098
  }
56045
56099
  },
56046
56100
  // Shared AI copy tells (engine/copy-tells) — the Wikipedia "Signs of AI
@@ -62344,23 +62398,24 @@ function narrowToFamily(commandName, available) {
62344
62398
  var schemaCommand2 = defineCommand204({
62345
62399
  meta: {
62346
62400
  name: "schema",
62347
- description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
62401
+ description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images search (or images.search)"
62348
62402
  },
62349
62403
  args: {
62350
62404
  command: {
62351
62405
  type: "positional",
62352
- description: "Command name (e.g. images.list). Omit to list all.",
62406
+ description: "Command path, as typed (images list) or dotted (images.list). Omit to list all.",
62353
62407
  required: false
62354
62408
  }
62355
62409
  },
62356
62410
  run: ({ args: args4 }) => {
62357
- const commandName = args4.command;
62358
- if (!commandName) {
62411
+ const words2 = args4._.filter((word) => typeof word === "string" && word.length > 0);
62412
+ if (words2.length === 0) {
62359
62413
  const schemas = listSchemas();
62360
62414
  writeJson({ ok: true, data: { commands: schemas } });
62361
62415
  return;
62362
62416
  }
62363
- const schema = getSchema(commandName);
62417
+ const commandName = words2.join(".");
62418
+ const schema = findSchema(words2);
62364
62419
  if (!schema) {
62365
62420
  const all = listSchemas();
62366
62421
  const available = narrowToFamily(commandName, all);
@@ -62810,7 +62865,7 @@ function clipImageLimitFor(args4) {
62810
62865
  if (args4.from !== "references") {
62811
62866
  return {
62812
62867
  max: 1,
62813
- refuse: (count) => `Animating one exact frame uses a single image \u2014 you passed ${count}. Pick one, or switch --from references to guide the shot with all of them.`
62868
+ refuse: (count2) => `Animating one exact frame uses a single image \u2014 you passed ${count2}. Pick one, or switch --from references to guide the shot with all of them.`
62814
62869
  };
62815
62870
  }
62816
62871
  if (!supportsReferenceToVideo(model)) {
@@ -62823,7 +62878,7 @@ function clipImageLimitFor(args4) {
62823
62878
  if (max === void 0) return void 0;
62824
62879
  return {
62825
62880
  max,
62826
- refuse: (count) => `${label} takes up to ${max} reference images \u2014 you passed ${count}. Drop the weakest ones.`
62881
+ refuse: (count2) => `${label} takes up to ${max} reference images \u2014 you passed ${count2}. Drop the weakest ones.`
62827
62882
  };
62828
62883
  }
62829
62884
  function clipModeFor(from) {
@@ -62848,7 +62903,7 @@ function buildAnimateBody(args4, prompt, imageIds) {
62848
62903
  return body;
62849
62904
  }
62850
62905
  function costHintsFor(body) {
62851
- const count = body.count ?? 1;
62906
+ const count2 = body.count ?? 1;
62852
62907
  const model = body.model ?? DEFAULT_VIDEO_GENERATE_MODEL;
62853
62908
  const perTake = estimateVideoCredits({
62854
62909
  model,
@@ -62865,9 +62920,9 @@ function costHintsFor(body) {
62865
62920
  // that gets re-routed — a frame-started clip never does.
62866
62921
  mayRouteToFal: body.clipMode === "references" && body.imageIds.length > 0
62867
62922
  });
62868
- const total = perTake * count;
62923
+ const total = perTake * count2;
62869
62924
  const hints2 = [
62870
- count === 1 ? `This clip costs about ${total} credits \u2014 roughly 45x an image.` : `${count} takes at about ${perTake} credits each \u2014 about ${total} credits.`
62925
+ count2 === 1 ? `This clip costs about ${total} credits \u2014 roughly 45x an image.` : `${count2} takes at about ${perTake} credits each \u2014 about ${total} credits.`
62871
62926
  ];
62872
62927
  const pinned = pinnedModelHint({ avatar: body.avatar, model: body.model, generateAudio: body.generateAudio });
62873
62928
  if (pinned) hints2.push(pinned);
@@ -62875,7 +62930,7 @@ function costHintsFor(body) {
62875
62930
  if (pace) hints2.push(pace);
62876
62931
  const silent = silentModelHint(model, body.prompt);
62877
62932
  if (silent) hints2.push(silent);
62878
- if (count > 1) {
62933
+ if (count2 > 1) {
62879
62934
  hints2.push(
62880
62935
  "Ask for more than one take only when the MOTION is the risk \u2014 for a look you are unsure about, iterate on the still first."
62881
62936
  );
@@ -63128,7 +63183,7 @@ function referenceLimitFor(model) {
63128
63183
  const label = MODEL_REGISTRY.image_generate[chosen]?.label ?? chosen;
63129
63184
  return {
63130
63185
  max,
63131
- refuse: (count) => max === 1 ? `${label} takes exactly one reference image \u2014 you passed ${count}. Pick the one that matters, or switch to a model that reads several.` : `${label} takes up to ${max} reference images \u2014 you passed ${count}. Drop the weakest ones.`
63186
+ refuse: (count2) => max === 1 ? `${label} takes exactly one reference image \u2014 you passed ${count2}. Pick the one that matters, or switch to a model that reads several.` : `${label} takes up to ${max} reference images \u2014 you passed ${count2}. Drop the weakest ones.`
63132
63187
  };
63133
63188
  }
63134
63189
  async function effectiveModel(pinned, skillId, fetchSkills = () => apiGet("/api/studio/skills")) {
@@ -66953,7 +67008,7 @@ function commandFor(root, argv) {
66953
67008
  const { command, resolved } = resolveCommand2(root, argv);
66954
67009
  return resolved ? command : null;
66955
67010
  }
66956
- function unknownFlagEnvelope(unknown, commandPath, suggestion) {
67011
+ function unknownFlagEnvelope(unknown, commandPath, suggestion, hasSchema) {
66957
67012
  const list = unknown.join(", ");
66958
67013
  return {
66959
67014
  ok: false,
@@ -66961,8 +67016,8 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
66961
67016
  code: "UNKNOWN_ARGUMENT",
66962
67017
  message: `${list} ${unknown.length > 1 ? "are not options" : "is not an option"} of \`baker ${commandPath}\`.`,
66963
67018
  fix: {
66964
- action: "check_schema",
66965
- explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`baker schema ${commandPath}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
67019
+ action: hasSchema ? "check_schema" : "check_help",
67020
+ explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`${hasSchema ? `baker schema ${commandPath}` : `baker ${commandPath} --help`}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
66966
67021
  },
66967
67022
  retryable: false
66968
67023
  }
@@ -66993,8 +67048,9 @@ function refuseUnknownFlags(root, argv) {
66993
67048
  const command = commandFor(root, argv);
66994
67049
  const first = unknown[0];
66995
67050
  const suggestion = command && first ? closestFlag(first, command) : null;
67051
+ const path48 = commandPathOf(root, argv);
66996
67052
  process.stdout.write(
66997
- `${JSON.stringify(unknownFlagEnvelope(unknown, commandPathOf(root, argv), suggestion), null, 2)}
67053
+ `${JSON.stringify(unknownFlagEnvelope(unknown, path48, suggestion, findSchema(path48.split(" ")) !== void 0), null, 2)}
66998
67054
  `
66999
67055
  );
67000
67056
  process.exit(1);