@koda-sl/baker-cli 0.328.0 → 0.329.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
@@ -1626,11 +1626,11 @@ var trackingParamsSetSchema = z3.object({
1626
1626
  message: `${both.join(", ")} is set as both a dynamic and a fixed parameter \u2014 LinkedIn appends each key once, so keep only one`
1627
1627
  });
1628
1628
  }
1629
- const count = Object.keys(p.dynamicValueParameters ?? {}).length + Object.keys(p.customValueParameters ?? {}).length;
1630
- if (count > LINKEDIN_LIMITS.trackingParams.parametersMax) {
1629
+ const count2 = Object.keys(p.dynamicValueParameters ?? {}).length + Object.keys(p.customValueParameters ?? {}).length;
1630
+ if (count2 > LINKEDIN_LIMITS.trackingParams.parametersMax) {
1631
1631
  ctx.addIssue({
1632
1632
  code: "custom",
1633
- message: `${count} parameters \u2014 keep it under ${LINKEDIN_LIMITS.trackingParams.parametersMax}`
1633
+ message: `${count2} parameters \u2014 keep it under ${LINKEDIN_LIMITS.trackingParams.parametersMax}`
1634
1634
  });
1635
1635
  }
1636
1636
  });
@@ -1937,8 +1937,8 @@ var leadFormQuestionSchema = z3.object({
1937
1937
  });
1938
1938
  }
1939
1939
  if (q.questionType === "MULTIPLE_CHOICE") {
1940
- const count = q.options?.length ?? 0;
1941
- if (count < LEAD.choiceOptionsMin || count > LEAD.choiceOptionsMax) {
1940
+ const count2 = q.options?.length ?? 0;
1941
+ if (count2 < LEAD.choiceOptionsMin || count2 > LEAD.choiceOptionsMax) {
1942
1942
  ctx.addIssue({
1943
1943
  code: "custom",
1944
1944
  path: ["options"],
@@ -10672,23 +10672,23 @@ var FALSE_RANGE_PATTERNS = [
10672
10672
  /\bdesde\s+([^,.;]{3,30}?)\s+hasta\s+([^,.;]{3,30}?)(?=[,.;]|$)/gi
10673
10673
  ];
10674
10674
  function countFalseRanges(text2) {
10675
- let count = 0;
10675
+ let count2 = 0;
10676
10676
  let first = "";
10677
10677
  for (const re2 of FALSE_RANGE_PATTERNS) {
10678
10678
  for (const m of text2.matchAll(re2)) {
10679
10679
  const left = m[1] ?? "";
10680
10680
  const right = m[2] ?? "";
10681
10681
  if (/\d/.test(left) || /\d/.test(right)) continue;
10682
- count++;
10682
+ count2++;
10683
10683
  if (!first) first = m[0] ?? "";
10684
10684
  }
10685
10685
  }
10686
- return { count, first };
10686
+ return { count: count2, first };
10687
10687
  }
10688
10688
  function countStaccato(text2) {
10689
10689
  const sentences = text2.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
10690
10690
  let run = 0;
10691
- let count = 0;
10691
+ let count2 = 0;
10692
10692
  let first = "";
10693
10693
  let runStart = 0;
10694
10694
  for (const [i, s] of sentences.entries()) {
@@ -10697,17 +10697,17 @@ function countStaccato(text2) {
10697
10697
  if (run === 0) runStart = i;
10698
10698
  run++;
10699
10699
  if (run === STACCATO_RUN) {
10700
- count++;
10700
+ count2++;
10701
10701
  if (!first) first = sentences.slice(runStart, i + 1).join(" ");
10702
10702
  }
10703
10703
  } else {
10704
10704
  run = 0;
10705
10705
  }
10706
10706
  }
10707
- return { count, first };
10707
+ return { count: count2, first };
10708
10708
  }
10709
10709
  function countTitleCaseHeadings(text2) {
10710
- let count = 0;
10710
+ let count2 = 0;
10711
10711
  let first = "";
10712
10712
  for (const m of text2.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
10713
10713
  const heading = (m[1] ?? "").trim();
@@ -10715,10 +10715,10 @@ function countTitleCaseHeadings(text2) {
10715
10715
  if (words2.length < 4) continue;
10716
10716
  const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
10717
10717
  if (capitalized / words2.length < 0.8) continue;
10718
- count++;
10718
+ count2++;
10719
10719
  if (!first) first = heading;
10720
10720
  }
10721
- return { count, first };
10721
+ return { count: count2, first };
10722
10722
  }
10723
10723
  function countInlineHeaderList(text2) {
10724
10724
  const matches = [...text2.matchAll(/^\s*(?:[-*+]|\d+\.)\s+\*\*[^*\n]{2,40}:?\*\*:?\s/gm)];
@@ -10728,15 +10728,15 @@ function countInlineHeaderList(text2) {
10728
10728
  var EMOJI = /[\u{1F300}-\u{1FAFF}]|[\u{2600}-\u{27BF}]|[\u{1F000}-\u{1F2FF}]|\u{FE0F}/gu;
10729
10729
  var CURLY = /[‘’“”]/g;
10730
10730
  function countPhraseTell(text2, id, phrases) {
10731
- let count = 0;
10731
+ let count2 = 0;
10732
10732
  let first = "";
10733
10733
  for (const phrase of phrases) {
10734
10734
  for (const m of text2.matchAll(matcherFor(id, phrase))) {
10735
- count++;
10735
+ count2++;
10736
10736
  if (!first) first = m[0] ?? "";
10737
10737
  }
10738
10738
  }
10739
- return { count, first };
10739
+ return { count: count2, first };
10740
10740
  }
10741
10741
  function countPattern(text2, re2) {
10742
10742
  const matches = text2.match(re2) ?? [];
@@ -10974,7 +10974,7 @@ function renderOutcome(data, lines) {
10974
10974
  const status = op.result?.status ?? "pending";
10975
10975
  counts.set(status, (counts.get(status) ?? 0) + 1);
10976
10976
  }
10977
- const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
10977
+ const summary = [...counts].map(([status, count2]) => `${count2} ${status}`).join(", ");
10978
10978
  lines.push(`Google Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
10979
10979
  for (const op of data.ops) {
10980
10980
  const status = op.result?.status;
@@ -11976,11 +11976,11 @@ function renderKeywordLimits(limits) {
11976
11976
  const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
11977
11977
  (reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
11978
11978
  );
11979
- const count = limits.keywords.size;
11979
+ const count2 = limits.keywords.size;
11980
11980
  return [
11981
11981
  {
11982
11982
  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).`
11983
+ 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
11984
  }
11985
11985
  ];
11986
11986
  }
@@ -16678,11 +16678,11 @@ function nestAddressFlags(row) {
16678
16678
  }
16679
16679
  function readNumericFlags(args4, row) {
16680
16680
  if (args4["conversion-count"] !== void 0 && args4["conversion-count"] !== "") {
16681
- const count = Number(args4["conversion-count"]);
16682
- if (!Number.isInteger(count) || count < 1) {
16681
+ const count2 = Number(args4["conversion-count"]);
16682
+ if (!Number.isInteger(count2) || count2 < 1) {
16683
16683
  failWriteValidation("--conversion-count must be a whole number of conversions, e.g. 3");
16684
16684
  }
16685
- row.conversionCount = count;
16685
+ row.conversionCount = count2;
16686
16686
  }
16687
16687
  if (args4.value !== void 0 && args4.value !== "") {
16688
16688
  const numeric = Number(args4.value);
@@ -17243,12 +17243,12 @@ function keywordKillHints(status) {
17243
17243
  const kills = typeof status === "string" && ["PAUSED", "REMOVED"].includes(status.toUpperCase());
17244
17244
  return kills ? KEYWORD_KILL_HINTS : [];
17245
17245
  }
17246
- function keywordFinalUrlHints(finalUrl, count) {
17246
+ function keywordFinalUrlHints(finalUrl, count2) {
17247
17247
  if (typeof finalUrl !== "string" || finalUrl.length === 0) {
17248
17248
  return [];
17249
17249
  }
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.`
17250
+ return count2 > 1 ? [
17251
+ `--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
17252
  ] : [
17253
17253
  "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
17254
  ];
@@ -21849,7 +21849,7 @@ function renderOutcome2(data, lines) {
21849
21849
  const status = op.result?.status ?? "pending";
21850
21850
  counts.set(status, (counts.get(status) ?? 0) + 1);
21851
21851
  }
21852
- const summary = [...counts].map(([status, count]) => `${count} ${status}`).join(", ");
21852
+ const summary = [...counts].map(([status, count2]) => `${count2} ${status}`).join(", ");
21853
21853
  lines.push(`LinkedIn Ads \u2014 published \xB7 ${summary || `${data.count} op(s)`} \xB7 ${data.mode}`);
21854
21854
  for (const op of data.ops ?? []) {
21855
21855
  const status = op.result?.status;
@@ -26986,12 +26986,12 @@ function compact(payload) {
26986
26986
  return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== void 0));
26987
26987
  }
26988
26988
  function batchNudge2(data) {
26989
- const count = data?.sameKindStaged;
26990
- if (!count) {
26989
+ const count2 = data?.sameKindStaged;
26990
+ if (!count2) {
26991
26991
  return [];
26992
26992
  }
26993
26993
  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.`
26994
+ `${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
26995
  ];
26996
26996
  }
26997
26997
  async function stageXOp(raw, hints2) {
@@ -35848,14 +35848,14 @@ function canvasDims(ar) {
35848
35848
  function fillPanel(label, w, h, out) {
35849
35849
  return `[${label}]scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h},setsar=1,fps=30[${out}]`;
35850
35850
  }
35851
- function splitStackArgs(count, axis, dims) {
35851
+ function splitStackArgs(count2, axis, dims) {
35852
35852
  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;
35853
+ const base = Math.floor(span / count2 / 2) * 2;
35854
+ const sizeAt = (i) => i === count2 - 1 ? span - base * (count2 - 1) : base;
35855
35855
  const inputs = [];
35856
35856
  const filt = [];
35857
35857
  let labels = "";
35858
- for (let i = 0; i < count; i++) {
35858
+ for (let i = 0; i < count2; i++) {
35859
35859
  inputs.push("-i", `{{in.c${i}}}`);
35860
35860
  const pw = axis === "horizontal" ? sizeAt(i) : dims.w;
35861
35861
  const ph = axis === "vertical" ? sizeAt(i) : dims.h;
@@ -35863,7 +35863,7 @@ function splitStackArgs(count, axis, dims) {
35863
35863
  labels += `[p${i}]`;
35864
35864
  }
35865
35865
  const stack = axis === "vertical" ? "vstack" : "hstack";
35866
- filt.push(`${labels}${stack}=inputs=${count}[v]`);
35866
+ filt.push(`${labels}${stack}=inputs=${count2}[v]`);
35867
35867
  return [...inputs, "-filter_complex", filt.join(";"), "-map", "[v]", "{{out.video}}"];
35868
35868
  }
35869
35869
  function overlayXY(position, marginPx) {
@@ -42537,13 +42537,13 @@ async function markDirectoryIsReal(dir) {
42537
42537
  var INK_ALPHA = 128;
42538
42538
  function inkLuma(rgba) {
42539
42539
  let sum = 0;
42540
- let count = 0;
42540
+ let count2 = 0;
42541
42541
  for (let i = 0; i + 3 < rgba.length; i += 4) {
42542
42542
  if (rgba[i + 3] < INK_ALPHA) continue;
42543
42543
  sum += 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
42544
- count += 1;
42544
+ count2 += 1;
42545
42545
  }
42546
- return count === 0 ? null : sum / count;
42546
+ return count2 === 0 ? null : sum / count2;
42547
42547
  }
42548
42548
  var LIGHT_INK_LUMA = 170;
42549
42549
  async function markInkIsLight(markPath) {
@@ -45008,185 +45008,6 @@ async function foldExperiment(root, target) {
45008
45008
  };
45009
45009
  }
45010
45010
 
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
45011
  // ../api/src/experiments/composition.ts
45191
45012
  import { z as z41 } from "zod";
45192
45013
  var experimentCompositionSchema = z41.object({
@@ -45251,6 +45072,25 @@ var experimentHypothesisOutcomeSchema = z43.enum([
45251
45072
  "unanswered"
45252
45073
  ]);
45253
45074
 
45075
+ // ../api/src/experiments/wait.ts
45076
+ var DAY_MS5 = 24 * 60 * 60 * 1e3;
45077
+ function count(value, unit) {
45078
+ return `about ${value} ${unit}${value === 1 ? "" : "s"}`;
45079
+ }
45080
+ function describeWait(earliestDecisionAt, now) {
45081
+ const days = (earliestDecisionAt - now) / DAY_MS5;
45082
+ if (days <= 0) return "any moment now";
45083
+ if (days < 1) return "within a day";
45084
+ if (days < 14) return count(Math.round(days), "day");
45085
+ if (days < 60) return count(Math.round(days / 7), "week");
45086
+ return count(Math.round(days / 30), "month");
45087
+ }
45088
+ function roughCount(count2) {
45089
+ if (count2 < 10) return `about ${Math.max(1, Math.round(count2)).toLocaleString("en-US")}`;
45090
+ const magnitude = 10 ** Math.max(1, Math.floor(Math.log10(count2)) - 1);
45091
+ return `about ${(Math.round(count2 / magnitude) * magnitude).toLocaleString("en-US")}`;
45092
+ }
45093
+
45254
45094
  // ../api/src/experiments/wire.ts
45255
45095
  import { z as z44 } from "zod";
45256
45096
  var slugSchema = z44.string().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a page slug, like `oferta`");
@@ -45470,7 +45310,17 @@ var experimentVerdictSchema = z44.enum([
45470
45310
  "keep_running",
45471
45311
  "winner",
45472
45312
  "no_difference",
45313
+ /**
45314
+ * Even the best case is under the lift the test was built to find, so there
45315
+ * is nothing left for it to discover: keep the original. The "is this a
45316
+ * failure" answer — not proven worse, but proven not worth running.
45317
+ */
45318
+ "cannot_win",
45473
45319
  "invalid",
45320
+ /**
45321
+ * Only on tests read before the stopping rule was one interval: a version
45322
+ * proven worse is now `winner` with `winner: "control"`, whenever it is seen.
45323
+ */
45474
45324
  "stopped_early_harmful",
45475
45325
  /**
45476
45326
  * Ended before the numbers could say anything. Never produced by the
@@ -45615,6 +45465,14 @@ var experimentStatusRowSchema = z44.object({
45615
45465
  precision: z44.number(),
45616
45466
  /** When the test could first say something. `null` with no traffic to project from. */
45617
45467
  earliestDecisionAt: z44.number().nullable(),
45468
+ /**
45469
+ * What the test is still missing before its verdict changes, if the gap it
45470
+ * is seeing holds: visitors across both arms, and the conversions they bring.
45471
+ * A test is gated on data, never on days — `earliestDecisionAt` is only this
45472
+ * divided by the page's pace. Null once it has spoken, before anyone has
45473
+ * arrived, or when no amount of traffic would settle it.
45474
+ */
45475
+ stillNeeded: z44.object({ visitors: z44.number(), conversions: z44.number() }).nullable().default(null),
45618
45476
  /**
45619
45477
  * True when no amount of this page's traffic would settle the comparison.
45620
45478
  * A different thing from a page that is merely slow, and the reason to end
@@ -45699,6 +45557,190 @@ var experimentFinishResponseSchema = z44.object({
45699
45557
  summary: z44.string()
45700
45558
  });
45701
45559
 
45560
+ // src/commands/experiment/hints.ts
45561
+ var LIVE_HINT = {
45562
+ 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.`,
45563
+ 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.`,
45564
+ 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.`,
45565
+ 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.`,
45566
+ 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.`,
45567
+ // Unreachable on a live test — only ending one produces it. The Record is
45568
+ // exhaustive by type so a verdict added upstream fails to compile here rather
45569
+ // than printing nothing.
45570
+ inconclusive: (row) => `${row.experimentId} was ended before it concluded and found nothing.`,
45571
+ 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.`
45572
+ };
45573
+ function describeWait2(row, now) {
45574
+ if (row.willNotConclude) {
45575
+ return "This page's traffic will not settle it at any point, so end it with `--keep original` and test a bigger change.";
45576
+ }
45577
+ if (row.earliestDecisionAt === null) {
45578
+ return "This page has produced no visitors yet, so there is nothing to project a finish date from.";
45579
+ }
45580
+ if (row.stillNeeded === null) {
45581
+ 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)}.`;
45582
+ }
45583
+ const needed = `It needs ${roughCount(row.stillNeeded.visitors)} more visitors (${roughCount(row.stillNeeded.conversions)} more conversions) for an answer if the gap holds \u2014 `;
45584
+ 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.`;
45585
+ }
45586
+ function pausedNote(row) {
45587
+ 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. ` : "";
45588
+ }
45589
+ function unlearnedHint(experiments) {
45590
+ const unlearned = experiments.filter(
45591
+ (row) => row.status === "finished" && row.learning === null && row.verdict !== "inconclusive"
45592
+ );
45593
+ if (unlearned.length === 0) return null;
45594
+ const ids = unlearned.map((row) => row.experimentId).join(", ");
45595
+ 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.`;
45596
+ }
45597
+ function buildStatusHints(experiments, now = Date.now()) {
45598
+ const hints2 = experiments.filter((row) => row.status !== "finished").map((row) => pausedNote(row) + LIVE_HINT[row.verdict](row, now));
45599
+ const unlearned = unlearnedHint(experiments);
45600
+ if (unlearned) hints2.push(unlearned);
45601
+ if (experiments.length === 0) {
45602
+ hints2.push(
45603
+ "No tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page has enough traffic for one before you build the alternative."
45604
+ );
45605
+ }
45606
+ return hints2;
45607
+ }
45608
+ function asLift(value) {
45609
+ return `+${Math.round(value * 100)}%`;
45610
+ }
45611
+ function horizonsHint(plan) {
45612
+ const settled = plan.horizons.filter((horizon) => horizon.detectableLift !== null);
45613
+ if (settled.length === 0) {
45614
+ 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.";
45615
+ }
45616
+ const parts = settled.map((horizon) => `${horizon.days} days \u2192 ${asLift(horizon.detectableLift ?? 0)}`);
45617
+ return `The smallest change each window could settle on this page's traffic: ${parts.join(", ")}. Design the alternative to be at least that different.`;
45618
+ }
45619
+ function conversionsHint(plan) {
45620
+ if (plan.conversions.length <= 1) return null;
45621
+ const others = plan.conversions.filter((entry) => entry.eventKey !== plan.goal);
45622
+ return `This page's visitors also went on to: ${others.map((entry) => `\`${entry.eventKey}\`${entry.name ? ` (\u201C${entry.name}\u201D)` : ""} \u2014 ${entry.convertingVisitors}`).join(
45623
+ ", "
45624
+ )}. Any of them can be the goal (--goal <key>); the named ones are reported beside it on every result, and decide nothing.`;
45625
+ }
45626
+ function pastTestLine(test) {
45627
+ const outcome = {
45628
+ supported: "the idea held",
45629
+ refuted_opposite: "the idea was WRONG \u2014 the change made it worse",
45630
+ refuted_no_effect: "no difference \u2014 the change did not matter",
45631
+ unanswered: "never answered"
45632
+ };
45633
+ const kept = test.survivor === "variant" ? "the new version was kept" : "the original was kept";
45634
+ const learning = test.learning ? ` Learning: ${test.learning}` : "";
45635
+ return `\u201C${test.hypothesis.change}\u201D (${test.experimentId}, ${test.ranForDays} days): ${outcome[test.hypothesisOutcome]}; ${kept}${test.againstVerdict ? " against the numbers" : ""}.${learning}`;
45636
+ }
45637
+ function pastTestsHint(plan) {
45638
+ if (plan.pastTests.length === 0) return null;
45639
+ const lines = plan.pastTests.slice(0, 5).map(pastTestLine);
45640
+ const more = plan.pastTests.length > 5 ? ` \u2026and ${plan.pastTests.length - 5} more \u2014 \`baker experiment history --landing <slug>\` lists them all.` : "";
45641
+ 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}`;
45642
+ }
45643
+ function buildPlanHints(plan) {
45644
+ const horizons = horizonsHint(plan);
45645
+ const past = pastTestsHint(plan);
45646
+ if (!plan.canRun) {
45647
+ return [
45648
+ plan.reason ?? "This test cannot run on this page.",
45649
+ ...horizons && plan.visitors > 0 ? [horizons] : [],
45650
+ ...past ? [past] : [],
45651
+ "A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
45652
+ ];
45653
+ }
45654
+ const hints2 = [
45655
+ ...past ? [past] : [],
45656
+ // First, because it changes how every number below should be read: a plan
45657
+ // sized on a stand-in is a guide, and the agent must say so to the client
45658
+ // rather than quote "about 70 days" as though it were measured.
45659
+ ...plan.warning ? [plan.warning] : [],
45660
+ `This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
45661
+ // Fires at the exact moment the hypothesis gets invented, whether or not
45662
+ // the agent read the family doc. `plan` says a question CAN be settled here
45663
+ // and says nothing about which question is worth asking — and a well-run
45664
+ // test of a bad idea spends the page's traffic and returns `no_difference`.
45665
+ "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.",
45666
+ // The base always works, so a missing integration is never a reason to
45667
+ // stop. Naming what it would have shown is worth more to the client than
45668
+ // either a silent gap or a run that waited.
45669
+ "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."
45670
+ ];
45671
+ const conversions = conversionsHint(plan);
45672
+ if (conversions) hints2.push(conversions);
45673
+ if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
45674
+ hints2.push(
45675
+ `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.`
45676
+ );
45677
+ }
45678
+ if (horizons) hints2.push(horizons);
45679
+ return hints2;
45680
+ }
45681
+ function buildHistoryHints(tests, landing) {
45682
+ if (tests.length === 0) {
45683
+ return [
45684
+ 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."
45685
+ ];
45686
+ }
45687
+ const hints2 = [];
45688
+ const supported = tests.filter((test) => test.hypothesisOutcome === "supported");
45689
+ const refuted = tests.filter((test) => test.hypothesisOutcome === "refuted_opposite");
45690
+ const noEffect = tests.filter((test) => test.hypothesisOutcome === "refuted_no_effect");
45691
+ if (supported.length > 0) {
45692
+ hints2.push(
45693
+ `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.`
45694
+ );
45695
+ }
45696
+ if (refuted.length > 0) {
45697
+ hints2.push(
45698
+ `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.`
45699
+ );
45700
+ }
45701
+ if (noEffect.length > 0) {
45702
+ hints2.push(
45703
+ `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.`
45704
+ );
45705
+ }
45706
+ const unlearned = tests.filter((test) => test.learning === null && test.verdict !== "inconclusive");
45707
+ if (unlearned.length > 0) {
45708
+ hints2.push(
45709
+ `${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"\`.`
45710
+ );
45711
+ }
45712
+ return hints2;
45713
+ }
45714
+ function buildStartHints(staged) {
45715
+ const hints2 = [];
45716
+ for (const entry of staged.drift) {
45717
+ hints2.push(driftHint(staged, entry));
45718
+ }
45719
+ if (staged.composition && staged.composition.forked.length === 0) {
45720
+ hints2.push(
45721
+ `\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.`
45722
+ );
45723
+ }
45724
+ if (staged.newForms.length > 0 && staged.variantGoal === void 0) {
45725
+ const forms = staged.newForms.map((flow) => `\u201C${flow}\u201D`).join(", ");
45726
+ hints2.push(
45727
+ `\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).`
45728
+ );
45729
+ }
45730
+ return hints2;
45731
+ }
45732
+ function driftHint(staged, entry) {
45733
+ 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\`).`;
45734
+ if (entry.reason === "trigger_differs") {
45735
+ 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}`;
45736
+ }
45737
+ if (entry.reason === "variant_only_missing" || entry.reason === "control_only_missing") {
45738
+ const missing = entry.reason === "variant_only_missing" ? staged.variant : staged.landing;
45739
+ 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}`;
45740
+ }
45741
+ 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}`;
45742
+ }
45743
+
45702
45744
  // src/commands/experiment/hypothesis.ts
45703
45745
  var SOURCES = experimentEvidenceSourceSchema.options.join(", ");
45704
45746
  function parseOne(raw) {
@@ -46056,7 +46098,7 @@ registerSchema({
46056
46098
  var statusCommand6 = defineCommand123({
46057
46099
  meta: {
46058
46100
  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."
46101
+ 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
46102
  },
46061
46103
  args: {
46062
46104
  id: { type: "string", description: "One test, by its id", required: false },
@@ -52809,7 +52851,7 @@ async function runMediaGroupLookup(input) {
52809
52851
  const data = await apiGet(`${input.path}?${query.toString()}`);
52810
52852
  const images = data.images.map((image) => compactImage(image));
52811
52853
  const videos = (data.videos ?? []).map((video) => compactVideo(video));
52812
- const count = images.length + videos.length;
52854
+ const count2 = images.length + videos.length;
52813
52855
  writeJson({
52814
52856
  ok: true,
52815
52857
  data: {
@@ -52819,11 +52861,11 @@ async function runMediaGroupLookup(input) {
52819
52861
  // place the campaign is named.
52820
52862
  groupTitle: data.groupTitle ?? null,
52821
52863
  groupUrl: data.groupUrl ?? null,
52822
- count,
52864
+ count: count2,
52823
52865
  images,
52824
52866
  videos
52825
52867
  },
52826
- hints: count > 1 ? ["These arrived together and are ordered. Read them in sequence before using any one of them alone."] : void 0
52868
+ hints: count2 > 1 ? ["These arrived together and are ordered. Read them in sequence before using any one of them alone."] : void 0
52827
52869
  });
52828
52870
  } catch (err) {
52829
52871
  if (err instanceof ApiError) {
@@ -53523,9 +53565,9 @@ function getDominantEdgeColor(data, width, height) {
53523
53565
  let maxCount = 0;
53524
53566
  let dominantColor = { r: 0, g: 0, b: 0 };
53525
53567
  for (const key in colorCount) {
53526
- const count = colorCount[key];
53527
- if (count > maxCount) {
53528
- maxCount = count;
53568
+ const count2 = colorCount[key];
53569
+ if (count2 > maxCount) {
53570
+ maxCount = count2;
53529
53571
  const parts = key.split(",").map(Number);
53530
53572
  dominantColor = { r: parts[0], g: parts[1], b: parts[2] };
53531
53573
  }
@@ -55969,10 +56011,10 @@ var ANALYZERS = [
55969
56011
  scope: "page",
55970
56012
  run: (text2, file) => {
55971
56013
  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 }];
56014
+ const count2 = (body.match(/[—]|(?<!-)--(?!-)(?=\S)/g) ?? []).length;
56015
+ if (count2 < EM_DASH_FLOOR) return [];
56016
+ if (body.length > count2 * EM_DASH_CHARS_PER_DASH) return [];
56017
+ return [{ id: "em-dash-overuse", snippet: `${count2} em-dashes in body copy`, file }];
55976
56018
  }
55977
56019
  },
55978
56020
  // Agent washing: an autonomy claim the page never scaffolds with a signal about
@@ -55999,25 +56041,25 @@ var ANALYZERS = [
55999
56041
  run: (text2, file) => {
56000
56042
  const body = stripHtmlToText(text2);
56001
56043
  const lower = body.toLowerCase();
56002
- let count = 0;
56044
+ let count2 = 0;
56003
56045
  let firstSample = "";
56004
56046
  for (const phrase of BUZZWORDS) {
56005
56047
  let from = 0;
56006
56048
  for (; ; ) {
56007
56049
  const idx = lower.indexOf(phrase, from);
56008
56050
  if (idx === -1) break;
56009
- count++;
56051
+ count2++;
56010
56052
  if (!firstSample) {
56011
56053
  firstSample = body.slice(Math.max(0, idx - 12), Math.min(body.length, idx + phrase.length + 12)).trim();
56012
56054
  }
56013
56055
  from = idx + phrase.length;
56014
56056
  }
56015
56057
  }
56016
- if (count === 0) return [];
56058
+ if (count2 === 0) return [];
56017
56059
  return [
56018
56060
  {
56019
56061
  id: "marketing-buzzword",
56020
- snippet: `${count} buzzword phrase${count === 1 ? "" : "s"}: "${firstSample}"`,
56062
+ snippet: `${count2} buzzword phrase${count2 === 1 ? "" : "s"}: "${firstSample}"`,
56021
56063
  file
56022
56064
  }
56023
56065
  ];
@@ -56029,18 +56071,18 @@ var ANALYZERS = [
56029
56071
  scope: "page",
56030
56072
  run: (text2, file) => {
56031
56073
  const body = stripHtmlToText(text2);
56032
- let count = 0;
56074
+ let count2 = 0;
56033
56075
  let firstSample = "";
56034
56076
  for (const m of body.matchAll(/\bNot an? [a-z][^.!?]{1,40}[.!]\s+[A-Z][^.!?]{1,60}[.!]/g)) {
56035
- count++;
56077
+ count2++;
56036
56078
  if (!firstSample) firstSample = cap2(m, 0).trim().slice(0, 80);
56037
56079
  }
56038
56080
  for (const m of body.matchAll(/\b[A-Z][^.!?]{4,80}[.!]\s+(No|Just)\s+[a-z][^.!?]{2,60}[.!]/g)) {
56039
- count++;
56081
+ count2++;
56040
56082
  if (!firstSample) firstSample = cap2(m, 0).trim().slice(0, 80);
56041
56083
  }
56042
- if (count < 3) return [];
56043
- return [{ id: "aphoristic-cadence", snippet: `${count} aphoristic constructions: "${firstSample}"`, file }];
56084
+ if (count2 < 3) return [];
56085
+ return [{ id: "aphoristic-cadence", snippet: `${count2} aphoristic constructions: "${firstSample}"`, file }];
56044
56086
  }
56045
56087
  },
56046
56088
  // Shared AI copy tells (engine/copy-tells) — the Wikipedia "Signs of AI
@@ -62810,7 +62852,7 @@ function clipImageLimitFor(args4) {
62810
62852
  if (args4.from !== "references") {
62811
62853
  return {
62812
62854
  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.`
62855
+ 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
62856
  };
62815
62857
  }
62816
62858
  if (!supportsReferenceToVideo(model)) {
@@ -62823,7 +62865,7 @@ function clipImageLimitFor(args4) {
62823
62865
  if (max === void 0) return void 0;
62824
62866
  return {
62825
62867
  max,
62826
- refuse: (count) => `${label} takes up to ${max} reference images \u2014 you passed ${count}. Drop the weakest ones.`
62868
+ refuse: (count2) => `${label} takes up to ${max} reference images \u2014 you passed ${count2}. Drop the weakest ones.`
62827
62869
  };
62828
62870
  }
62829
62871
  function clipModeFor(from) {
@@ -62848,7 +62890,7 @@ function buildAnimateBody(args4, prompt, imageIds) {
62848
62890
  return body;
62849
62891
  }
62850
62892
  function costHintsFor(body) {
62851
- const count = body.count ?? 1;
62893
+ const count2 = body.count ?? 1;
62852
62894
  const model = body.model ?? DEFAULT_VIDEO_GENERATE_MODEL;
62853
62895
  const perTake = estimateVideoCredits({
62854
62896
  model,
@@ -62865,9 +62907,9 @@ function costHintsFor(body) {
62865
62907
  // that gets re-routed — a frame-started clip never does.
62866
62908
  mayRouteToFal: body.clipMode === "references" && body.imageIds.length > 0
62867
62909
  });
62868
- const total = perTake * count;
62910
+ const total = perTake * count2;
62869
62911
  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.`
62912
+ 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
62913
  ];
62872
62914
  const pinned = pinnedModelHint({ avatar: body.avatar, model: body.model, generateAudio: body.generateAudio });
62873
62915
  if (pinned) hints2.push(pinned);
@@ -62875,7 +62917,7 @@ function costHintsFor(body) {
62875
62917
  if (pace) hints2.push(pace);
62876
62918
  const silent = silentModelHint(model, body.prompt);
62877
62919
  if (silent) hints2.push(silent);
62878
- if (count > 1) {
62920
+ if (count2 > 1) {
62879
62921
  hints2.push(
62880
62922
  "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
62923
  );
@@ -63128,7 +63170,7 @@ function referenceLimitFor(model) {
63128
63170
  const label = MODEL_REGISTRY.image_generate[chosen]?.label ?? chosen;
63129
63171
  return {
63130
63172
  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.`
63173
+ 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
63174
  };
63133
63175
  }
63134
63176
  async function effectiveModel(pinned, skillId, fetchSkills = () => apiGet("/api/studio/skills")) {