@koda-sl/baker-cli 0.287.0-dev.93be96120 → 0.290.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  ulid,
59
59
  validateCanvasDeep,
60
60
  ytDlpBlockSignal
61
- } from "./chunk-LN5O53BG.js";
61
+ } from "./chunk-NNQDWCFG.js";
62
62
  import {
63
63
  csvOrJson,
64
64
  daysAgoIso,
@@ -10509,14 +10509,14 @@ function collectionLine(collection) {
10509
10509
  }
10510
10510
  function accountsInTree(nodes) {
10511
10511
  const accounts = /* @__PURE__ */ new Set();
10512
- const walk = (node) => {
10512
+ const walk2 = (node) => {
10513
10513
  accounts.add(node.customerId);
10514
10514
  for (const child of node.children) {
10515
- walk(child);
10515
+ walk2(child);
10516
10516
  }
10517
10517
  };
10518
10518
  for (const node of nodes) {
10519
- walk(node);
10519
+ walk2(node);
10520
10520
  }
10521
10521
  return accounts;
10522
10522
  }
@@ -10683,11 +10683,11 @@ function parseKeywordEntry(raw, defaultMatch) {
10683
10683
  function rawTextEntries(value, rawArgs) {
10684
10684
  return repeatedValues(rawArgs, "text", value).flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
10685
10685
  }
10686
- function rawFileEntries(path42) {
10687
- if (typeof path42 !== "string" || path42.length === 0) {
10686
+ function rawFileEntries(path44) {
10687
+ if (typeof path44 !== "string" || path44.length === 0) {
10688
10688
  return [];
10689
10689
  }
10690
- return readFileSync2(path42, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
10690
+ return readFileSync2(path44, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
10691
10691
  }
10692
10692
  function keywordEntries(args, rawArgs) {
10693
10693
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -10710,14 +10710,14 @@ function keywordEntries(args, rawArgs) {
10710
10710
  }
10711
10711
  return entries;
10712
10712
  }
10713
- function loadJsonFileArg(path42) {
10714
- if (typeof path42 !== "string" || path42.length === 0) {
10713
+ function loadJsonFileArg(path44) {
10714
+ if (typeof path44 !== "string" || path44.length === 0) {
10715
10715
  return {};
10716
10716
  }
10717
- const inline = path42.trimStart().startsWith("{");
10718
- const source = inline ? "inline JSON" : path42;
10717
+ const inline = path44.trimStart().startsWith("{");
10718
+ const source = inline ? "inline JSON" : path44;
10719
10719
  try {
10720
- const parsed = JSON.parse(inline ? path42 : readFileSync2(path42, "utf8"));
10720
+ const parsed = JSON.parse(inline ? path44 : readFileSync2(path44, "utf8"));
10721
10721
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
10722
10722
  failWriteValidation(`${source} must contain a JSON object`);
10723
10723
  }
@@ -10854,10 +10854,10 @@ async function stageUpdate(kind, customerId, target, payload, hints2) {
10854
10854
  async function stageTarget(kind, customerId, target, hints2) {
10855
10855
  await stageGoogleOp({ kind, customerId, target }, hints2);
10856
10856
  }
10857
- async function draftAction(path42, body, chat) {
10857
+ async function draftAction(path44, body, chat) {
10858
10858
  try {
10859
10859
  const chatId = resolveChatId(chat);
10860
- const response = await apiPost(path42, { chatId, ...body });
10860
+ const response = await apiPost(path44, { chatId, ...body });
10861
10861
  writeJsonEnvelope(response);
10862
10862
  } catch (err) {
10863
10863
  handleGoogleError(err);
@@ -12799,6 +12799,25 @@ var CAPTURED_WITHOUT_ROLE = [
12799
12799
  "targetid"
12800
12800
  ].sort();
12801
12801
 
12802
+ // ../api/src/analytics/conversionNames.ts
12803
+ function normalizeConversionName(name) {
12804
+ return name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter((word) => word !== "").map((word) => word.length > 3 && word.endsWith("s") ? word.slice(0, -1) : word).join(" ");
12805
+ }
12806
+ function findNameCollision(name, existing) {
12807
+ const normalized = normalizeConversionName(name);
12808
+ return existing.find((candidate) => candidate !== name && normalizeConversionName(candidate) === normalized) ?? null;
12809
+ }
12810
+ var STANDARD_CONVERSION_NAMES = [
12811
+ { name: "Lead", meaning: "Somebody handed over contact details" },
12812
+ { name: "Booked a call", meaning: "A meeting or demo was scheduled" },
12813
+ { name: "Quote requested", meaning: "Somebody asked what it would cost" },
12814
+ { name: "Purchase", meaning: "Money changed hands" },
12815
+ { name: "Signed up", meaning: "An account or trial was created" },
12816
+ { name: "Subscribed", meaning: "Somebody opted in to hear from them again" },
12817
+ { name: "Downloaded", meaning: "A guide, price list or asset was taken" },
12818
+ { name: "Contacted us", meaning: "A call, WhatsApp or email started from the page" }
12819
+ ];
12820
+
12802
12821
  // ../api/src/analytics/conversions.ts
12803
12822
  var CONVERSION_COUNT_MODES = ["once_per_visit", "every_time"];
12804
12823
  var PRIMARY_CONVERSION_TRIGGER = {
@@ -12820,6 +12839,9 @@ var MAX_CONVERSION_NAME = 80;
12820
12839
 
12821
12840
  // ../api/src/analytics/eventKey.ts
12822
12841
  var SEP = ":";
12842
+ function formEventKey(flowSlug, nodeId, triggerId) {
12843
+ return ["form", flowSlug, nodeId, triggerId].join(SEP);
12844
+ }
12823
12845
  function parseEventKey(key) {
12824
12846
  const cut = key.indexOf(SEP);
12825
12847
  if (cut <= 0) return null;
@@ -14363,6 +14385,19 @@ var analyticsTimelineRowSchema = z28.object({
14363
14385
  * what made the server-sent half of the feed look unattached to the rest.
14364
14386
  */
14365
14387
  knownBy: z28.string().default(""),
14388
+ /**
14389
+ * What this row counts as under the company's own conversion definitions, or
14390
+ * empty when it counts as nothing.
14391
+ *
14392
+ * The store's own key, projected rather than rebuilt here, so the feed and
14393
+ * the reports cannot disagree about what one event was. Two rows carrying the
14394
+ * same non-empty key are **one** conversion — the second is stored, listed,
14395
+ * and marked as already counted rather than hidden, because a sender who
14396
+ * posted a deal twice needs to see that the second request arrived.
14397
+ *
14398
+ * Only `event_stream` fills it; empty everywhere else.
14399
+ */
14400
+ conversionKey: z28.string().default(""),
14366
14401
  /**
14367
14402
  * `email` | `phone` — which of the two they gave.
14368
14403
  *
@@ -14403,6 +14438,17 @@ var analyticsTimelineRowSchema = z28.object({
14403
14438
  runId: z28.string(),
14404
14439
  engagementMs: z28.number().int().nonnegative(),
14405
14440
  scrollDepth: z28.number().int().nonnegative(),
14441
+ /**
14442
+ * The A/B test this row was stamped into, and which side the edge served —
14443
+ * `a` for the page as it is, `b` for the new variant.
14444
+ *
14445
+ * Set only on rows a page under test produced, which is how a person's story
14446
+ * can say which version they were shown. Empty on a server-sent row: a CRM
14447
+ * knows nothing about the split, and the conversion it posts is attributed
14448
+ * to a side by WHO did it (see `experiment_conversions`), never by a stamp.
14449
+ */
14450
+ experimentId: z28.string().default(""),
14451
+ variant: z28.string().default(""),
14406
14452
  /** `success` | `failed` | `""` for a dispatch that has not settled yet. */
14407
14453
  outcome: z28.string(),
14408
14454
  /** What the destination said when it refused. Empty on success. */
@@ -15227,17 +15273,17 @@ function ignoredColumnHints(columns) {
15227
15273
  `Ignored ${columns.length} column(s) that are not part of a conversion: ${columns.join(", ")}. A conversion carries an order id, a date, and something to match on: a click id (gclid / gbraid / wbraid), an email, a phone, or all four of givenName + familyName + country + postalCode. Optionally a value + currency, conversionCount, and eventSource (WEB / APP / IN_STORE / PHONE / OTHER). A column named just \`Name\` is ignored on purpose \u2014 Google needs the first and last name separately, so split it before re-running.`
15228
15274
  ];
15229
15275
  }
15230
- function readRowsFile(path42) {
15231
- const inline = path42.trimStart().startsWith("[") || path42.trimStart().startsWith("{");
15276
+ function readRowsFile(path44) {
15277
+ const inline = path44.trimStart().startsWith("[") || path44.trimStart().startsWith("{");
15232
15278
  let text2;
15233
15279
  try {
15234
- text2 = inline ? path42 : readFileSync4(path42, "utf8");
15280
+ text2 = inline ? path44 : readFileSync4(path44, "utf8");
15235
15281
  } catch (err) {
15236
- failWriteValidation(`could not read ${path42}: ${err instanceof Error ? err.message : String(err)}`);
15282
+ failWriteValidation(`could not read ${path44}: ${err instanceof Error ? err.message : String(err)}`);
15237
15283
  }
15238
15284
  const parsed = parseConversionRowsText(text2);
15239
15285
  if (!parsed.ok) {
15240
- failWriteValidation(`${inline ? "the conversions passed inline" : path42}: ${parsed.error}`);
15286
+ failWriteValidation(`${inline ? "the conversions passed inline" : path44}: ${parsed.error}`);
15241
15287
  }
15242
15288
  return { rows: parsed.rows, ignoredColumns: parsed.ignoredColumns };
15243
15289
  }
@@ -17076,19 +17122,19 @@ function failWriteValidation2(message) {
17076
17122
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
17077
17123
  process.exit(1);
17078
17124
  }
17079
- function loadJsonFileArg2(path42) {
17080
- if (typeof path42 !== "string" || path42.length === 0) {
17125
+ function loadJsonFileArg2(path44) {
17126
+ if (typeof path44 !== "string" || path44.length === 0) {
17081
17127
  return {};
17082
17128
  }
17083
17129
  try {
17084
- const parsed = JSON.parse(readFileSync5(path42, "utf8"));
17130
+ const parsed = JSON.parse(readFileSync5(path44, "utf8"));
17085
17131
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
17086
- failWriteValidation2(`${path42} must contain a JSON object`);
17132
+ failWriteValidation2(`${path44} must contain a JSON object`);
17087
17133
  }
17088
17134
  return parsed;
17089
17135
  } catch (err) {
17090
17136
  if (err instanceof SyntaxError) {
17091
- failWriteValidation2(`${path42} is not valid JSON: ${err.message}`);
17137
+ failWriteValidation2(`${path44} is not valid JSON: ${err.message}`);
17092
17138
  }
17093
17139
  throw err;
17094
17140
  }
@@ -17173,15 +17219,15 @@ function parseLocaleFlag(value) {
17173
17219
  }
17174
17220
  return { language: match[1], country: match[2].toUpperCase() };
17175
17221
  }
17176
- function loadTargetingFileArg(path42) {
17177
- if (typeof path42 !== "string" || path42.length === 0) {
17222
+ function loadTargetingFileArg(path44) {
17223
+ if (typeof path44 !== "string" || path44.length === 0) {
17178
17224
  return void 0;
17179
17225
  }
17180
- const parsed = loadJsonFileArg2(path42);
17226
+ const parsed = loadJsonFileArg2(path44);
17181
17227
  const criteria = parsed.targetingCriteria ?? parsed;
17182
17228
  if (!criteria.include) {
17183
17229
  failWriteValidation2(
17184
- `${path42} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
17230
+ `${path44} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
17185
17231
  );
17186
17232
  }
17187
17233
  return criteria;
@@ -17216,14 +17262,14 @@ function parseCsvLine(line) {
17216
17262
  cells.push(current);
17217
17263
  return cells.map((cell3) => cell3.trim());
17218
17264
  }
17219
- function parseListFileArg(path42, maxRows) {
17220
- if (typeof path42 !== "string" || path42.length === 0) {
17265
+ function parseListFileArg(path44, maxRows) {
17266
+ if (typeof path44 !== "string" || path44.length === 0) {
17221
17267
  return void 0;
17222
17268
  }
17223
- const raw = readFileSync5(path42, "utf8");
17269
+ const raw = readFileSync5(path44, "utf8");
17224
17270
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
17225
17271
  if (lines.length < 2) {
17226
- failWriteValidation2(`${path42} needs a header row and at least one data row`);
17272
+ failWriteValidation2(`${path44} needs a header row and at least one data row`);
17227
17273
  }
17228
17274
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
17229
17275
  const rows = [];
@@ -17242,7 +17288,7 @@ function parseListFileArg(path42, maxRows) {
17242
17288
  }
17243
17289
  }
17244
17290
  if (rows.length > maxRows) {
17245
- failWriteValidation2(`${path42} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
17291
+ failWriteValidation2(`${path44} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
17246
17292
  }
17247
17293
  return { columns, rows };
17248
17294
  }
@@ -17338,11 +17384,11 @@ function readPositionals(args) {
17338
17384
  function splitIdList(raw) {
17339
17385
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
17340
17386
  }
17341
- function idsFileEntries(path42) {
17342
- if (typeof path42 !== "string" || path42.length === 0) {
17387
+ function idsFileEntries(path44) {
17388
+ if (typeof path44 !== "string" || path44.length === 0) {
17343
17389
  return [];
17344
17390
  }
17345
- return readFileSync5(path42, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
17391
+ return readFileSync5(path44, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
17346
17392
  }
17347
17393
  function requireTargets(args, entity) {
17348
17394
  const positionals = readPositionals(args);
@@ -20062,9 +20108,9 @@ function compactRow(row) {
20062
20108
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
20063
20109
  };
20064
20110
  }
20065
- function readPath(row, path42) {
20111
+ function readPath(row, path44) {
20066
20112
  let current = row;
20067
- for (const segment of path42.split(".")) {
20113
+ for (const segment of path44.split(".")) {
20068
20114
  const record = asRecord2(current);
20069
20115
  if (!record) return void 0;
20070
20116
  current = record[segment];
@@ -20074,10 +20120,10 @@ function readPath(row, path42) {
20074
20120
  function projectFields(rows, paths) {
20075
20121
  return rows.map((row) => {
20076
20122
  const projected = {};
20077
- for (const path42 of paths) {
20078
- const value = readPath(row, path42);
20123
+ for (const path44 of paths) {
20124
+ const value = readPath(row, path44);
20079
20125
  if (value !== void 0) {
20080
- projected[path42] = value;
20126
+ projected[path44] = value;
20081
20127
  }
20082
20128
  }
20083
20129
  return projected;
@@ -20332,14 +20378,14 @@ function collectionLine2(collection) {
20332
20378
  }
20333
20379
  function accountsInTree2(nodes) {
20334
20380
  const accounts = /* @__PURE__ */ new Set();
20335
- const walk = (node) => {
20381
+ const walk2 = (node) => {
20336
20382
  accounts.add(node.accountId);
20337
20383
  for (const child of node.children) {
20338
- walk(child);
20384
+ walk2(child);
20339
20385
  }
20340
20386
  };
20341
20387
  for (const node of nodes) {
20342
- walk(node);
20388
+ walk2(node);
20343
20389
  }
20344
20390
  return accounts;
20345
20391
  }
@@ -21673,11 +21719,11 @@ var updateStatusSchema = z29.enum(UPDATE_STATUSES);
21673
21719
  function currencyMinimums2(currencyCode) {
21674
21720
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
21675
21721
  }
21676
- function validateDailyBudgetFloor(money, ctx, path42) {
21722
+ function validateDailyBudgetFloor(money, ctx, path44) {
21677
21723
  if (money?.currencyCode) {
21678
21724
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
21679
21725
  if (Number(money.amount) < min) {
21680
- ctx.addIssue({ code: "custom", path: path42, message: `below the ${min} ${money.currencyCode} daily minimum` });
21726
+ ctx.addIssue({ code: "custom", path: path44, message: `below the ${min} ${money.currencyCode} daily minimum` });
21681
21727
  }
21682
21728
  }
21683
21729
  }
@@ -22437,19 +22483,19 @@ function failWriteValidation3(message) {
22437
22483
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
22438
22484
  process.exit(1);
22439
22485
  }
22440
- function loadJsonFileArg3(path42) {
22441
- if (typeof path42 !== "string" || path42.length === 0) {
22486
+ function loadJsonFileArg3(path44) {
22487
+ if (typeof path44 !== "string" || path44.length === 0) {
22442
22488
  return {};
22443
22489
  }
22444
22490
  try {
22445
- const parsed = JSON.parse(readFileSync9(path42, "utf8"));
22491
+ const parsed = JSON.parse(readFileSync9(path44, "utf8"));
22446
22492
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
22447
- failWriteValidation3(`${path42} must contain a JSON object`);
22493
+ failWriteValidation3(`${path44} must contain a JSON object`);
22448
22494
  }
22449
22495
  return parsed;
22450
22496
  } catch (err) {
22451
22497
  if (err instanceof SyntaxError) {
22452
- failWriteValidation3(`${path42} is not valid JSON: ${err.message}`);
22498
+ failWriteValidation3(`${path44} is not valid JSON: ${err.message}`);
22453
22499
  }
22454
22500
  throw err;
22455
22501
  }
@@ -25362,14 +25408,14 @@ function collectionLine3(collection) {
25362
25408
  }
25363
25409
  function accountsInTree3(nodes) {
25364
25410
  const accounts = /* @__PURE__ */ new Set();
25365
- const walk = (node) => {
25411
+ const walk2 = (node) => {
25366
25412
  accounts.add(node.accountId);
25367
25413
  for (const child of node.children) {
25368
- walk(child);
25414
+ walk2(child);
25369
25415
  }
25370
25416
  };
25371
25417
  for (const node of nodes) {
25372
- walk(node);
25418
+ walk2(node);
25373
25419
  }
25374
25420
  return accounts;
25375
25421
  }
@@ -27184,6 +27230,9 @@ Full guides: __tooling__/docs/tools/baker/ads-<platform>.md (google|meta|linkedi
27184
27230
  }
27185
27231
  });
27186
27232
 
27233
+ // src/commands/analytics/index.ts
27234
+ import { readFileSync as readFileSync12 } from "fs";
27235
+
27187
27236
  // ../api/src/event-ingest/arrivals.ts
27188
27237
  import { z as z31 } from "zod";
27189
27238
 
@@ -27997,50 +28046,325 @@ var ingestEnvelopeSchema = z32.object({ ok: z32.literal(true), data: ingestRespo
27997
28046
  // src/commands/analytics/index.ts
27998
28047
  import { defineCommand as defineCommand95 } from "citty";
27999
28048
 
28049
+ // src/commands/flows/shared.ts
28050
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
28051
+ import { join as join2 } from "path";
28052
+ var FLOWS_DIR = "src/lib/flow-engine/flows";
28053
+ function flowsDir() {
28054
+ let dir = process.cwd();
28055
+ for (let i = 0; i < 6; i++) {
28056
+ const candidate = join2(dir, FLOWS_DIR);
28057
+ if (existsSync3(candidate)) {
28058
+ return candidate;
28059
+ }
28060
+ const parent = join2(dir, "..");
28061
+ if (parent === dir) {
28062
+ break;
28063
+ }
28064
+ dir = parent;
28065
+ }
28066
+ return join2(process.cwd(), FLOWS_DIR);
28067
+ }
28068
+ function failLocal(message) {
28069
+ writeJson({ ok: false, error: { code: "NOT_FOUND", message } });
28070
+ process.exit(1);
28071
+ }
28072
+ function listFlowSlugs() {
28073
+ const dir = flowsDir();
28074
+ if (!existsSync3(dir)) {
28075
+ return [];
28076
+ }
28077
+ return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
28078
+ }
28079
+ function flowDataPath(slug) {
28080
+ return join2(flowsDir(), slug, "_data.json");
28081
+ }
28082
+ function writeFlowTree(slug, tree) {
28083
+ writeFileSync3(flowDataPath(slug), `${JSON.stringify(tree, null, 2)}
28084
+ `, "utf-8");
28085
+ }
28086
+ function walkNodes(node, acc = []) {
28087
+ if (!node || typeof node !== "object") return acc;
28088
+ acc.push(node);
28089
+ if (Array.isArray(node.children)) {
28090
+ for (const child of node.children) walkNodes(child, acc);
28091
+ }
28092
+ return acc;
28093
+ }
28094
+ function collectSideEffects(tree) {
28095
+ return walkNodes(tree).flatMap(
28096
+ (node) => Array.isArray(node.sideEffects) ? node.sideEffects.filter((sideEffect) => Boolean(sideEffect) && typeof sideEffect === "object").map((sideEffect) => ({ node, sideEffect })) : []
28097
+ );
28098
+ }
28099
+ function flowExists(slug) {
28100
+ return existsSync3(join2(flowsDir(), slug, "_data.json"));
28101
+ }
28102
+ function readFlowTree(slug) {
28103
+ const path44 = join2(flowsDir(), slug, "_data.json");
28104
+ if (!existsSync3(path44)) {
28105
+ failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
28106
+ }
28107
+ try {
28108
+ return JSON.parse(readFileSync11(path44, "utf-8"));
28109
+ } catch (error) {
28110
+ failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
28111
+ }
28112
+ }
28113
+ function isConfigured(value) {
28114
+ return typeof value === "string" && value.length > 0;
28115
+ }
28116
+ function nodeMeta(record) {
28117
+ const nodeId = typeof record.id === "string" ? record.id : "";
28118
+ const nodeName = typeof record.name === "string" ? record.name : void 0;
28119
+ return { nodeId, ...nodeName ? { nodeName } : {} };
28120
+ }
28121
+ function widgetNodeStatus(record, meta) {
28122
+ const nodeData = record.nodeData;
28123
+ const nodeType = nodeData?.type;
28124
+ if (typeof nodeType !== "string" || !FLOW_RESOURCE_NODE_TYPES.includes(nodeType)) {
28125
+ return null;
28126
+ }
28127
+ const external = nodeData?.form?.external;
28128
+ const selected = external != null && typeof external === "object";
28129
+ const name = selected ? external.name : void 0;
28130
+ return {
28131
+ ...meta,
28132
+ target: "node",
28133
+ kind: nodeType,
28134
+ status: selected ? `${nodeType} \u2014 selected${typeof name === "string" ? ` "${name}"` : ""}` : `${nodeType} \u2014 not selected`,
28135
+ needsInput: !selected
28136
+ };
28137
+ }
28138
+ function sideEffectNeedsInput(raw) {
28139
+ const effect = raw;
28140
+ const type = effect?.type;
28141
+ if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
28142
+ return false;
28143
+ }
28144
+ const configured = isConfigured(effect?.encryptedConfig);
28145
+ if (!isFlowOauthSideEffect(type)) return !configured;
28146
+ return !isConfigured(effect?.oauthProviderId) || !configured;
28147
+ }
28148
+ function sideEffectStatus(raw, meta) {
28149
+ const effect = raw;
28150
+ const type = effect?.type;
28151
+ if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
28152
+ return null;
28153
+ }
28154
+ const configured = isConfigured(effect?.encryptedConfig);
28155
+ const oauth = isFlowOauthSideEffect(type);
28156
+ const connected = isConfigured(effect?.oauthProviderId);
28157
+ const needs = FLOW_SECRET_FIELDS[type];
28158
+ const status = oauth ? `${type} \u2014 connection ${connected ? "[connected]" : "[needs connection]"}, config ${configured ? "[set]" : "[missing]"}` : configured ? `${type} \u2014 [configured]` : `${type} \u2014 [missing]${needs.length > 0 ? ` (needs: ${needs.join(", ")})` : ""}`;
28159
+ return {
28160
+ ...meta,
28161
+ target: "sideEffect",
28162
+ ...typeof effect?.id === "string" ? { sideEffectId: effect.id } : {},
28163
+ kind: type,
28164
+ status,
28165
+ needsInput: sideEffectNeedsInput(effect)
28166
+ };
28167
+ }
28168
+ function requestFlowInputCall(slug, status) {
28169
+ const args = status.target === "node" ? `{ target: "node", flowSlug: "${slug}", nodeId: "${status.nodeId}", nodeType: "${status.kind}" }` : `{ target: "sideEffect", flowSlug: "${slug}", nodeId: "${status.nodeId}", sideEffectId: "${status.sideEffectId}", sideEffectType: "${status.kind}" }`;
28170
+ return `request_flow_input with ${args}`;
28171
+ }
28172
+ function collectConfigStatus(node, acc) {
28173
+ if (!node || typeof node !== "object") {
28174
+ return;
28175
+ }
28176
+ const record = node;
28177
+ const meta = nodeMeta(record);
28178
+ const widget = widgetNodeStatus(record, meta);
28179
+ if (widget) {
28180
+ acc.push(widget);
28181
+ }
28182
+ if (Array.isArray(record.sideEffects)) {
28183
+ for (const raw of record.sideEffects) {
28184
+ const status = sideEffectStatus(raw, meta);
28185
+ if (status) {
28186
+ acc.push(status);
28187
+ }
28188
+ }
28189
+ }
28190
+ if (Array.isArray(record.children)) {
28191
+ for (const child of record.children) {
28192
+ collectConfigStatus(child, acc);
28193
+ }
28194
+ }
28195
+ }
28196
+ function redactTree(node) {
28197
+ if (Array.isArray(node)) {
28198
+ return node.map(redactTree);
28199
+ }
28200
+ if (!node || typeof node !== "object") {
28201
+ return node;
28202
+ }
28203
+ const out = {};
28204
+ for (const [key, value] of Object.entries(node)) {
28205
+ if (key === "encryptedConfig") {
28206
+ out[key] = isConfigured(value) ? "[configured]" : value;
28207
+ } else {
28208
+ out[key] = redactTree(value);
28209
+ }
28210
+ }
28211
+ return out;
28212
+ }
28213
+ function stepNameKey(name) {
28214
+ return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
28215
+ }
28216
+ function isUsableStepNameKey(key) {
28217
+ return /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*$/u.test(key);
28218
+ }
28219
+
28000
28220
  // src/commands/analytics/conversionArgs.ts
28001
28221
  var ConversionArgsError = class extends Error {
28002
28222
  };
28223
+ function allValues(flag, argv) {
28224
+ const values = [];
28225
+ for (let i = 0; i < argv.length; i += 1) {
28226
+ const arg = argv[i];
28227
+ if (arg === flag) {
28228
+ const next = argv[i + 1];
28229
+ if (next !== void 0 && !next.startsWith("--")) values.push(next);
28230
+ continue;
28231
+ }
28232
+ if (arg.startsWith(`${flag}=`)) values.push(arg.slice(flag.length + 1));
28233
+ }
28234
+ return values.map((value) => value.trim()).filter((value) => value !== "");
28235
+ }
28003
28236
  function onlyOnce(value, flag, argv) {
28004
28237
  if (value === void 0) return void 0;
28005
28238
  const given = argv.filter((arg) => arg === flag || arg.startsWith(`${flag}=`)).length;
28006
28239
  if (given > 1) {
28007
28240
  throw new ConversionArgsError(
28008
- `${flag} was given ${given} times and only the last would have been used. Count one outcome per call.`
28241
+ `${flag} was given ${given} times and only the last would have been used. It takes one value.`
28009
28242
  );
28010
28243
  }
28011
28244
  const trimmed = String(value).trim();
28012
28245
  return trimmed === "" ? void 0 : trimmed;
28013
28246
  }
28014
- function parseConversionsArgs(raw, argv) {
28015
- const eventKey = onlyOnce(raw.event, "--event", argv);
28016
- const name = onlyOnce(raw.name, "--name", argv);
28017
- const countMode = onlyOnce(raw["count-mode"], "--count-mode", argv);
28018
- const remove = onlyOnce(raw.remove, "--remove", argv);
28019
- if (eventKey !== void 0 && name === void 0) {
28020
- throw new ConversionArgsError("--event needs --name: a conversion nobody named is a row labelled with a UUID.");
28247
+ function repeatable(flag, cittyValue, argv) {
28248
+ const fromArgv = allValues(flag, argv);
28249
+ if (fromArgv.length > 0) return fromArgv;
28250
+ if (cittyValue === void 0) return [];
28251
+ const trimmed = String(cittyValue).trim();
28252
+ return trimmed === "" ? [] : [trimmed];
28253
+ }
28254
+ function checkFlagShape(args) {
28255
+ if (args.eventKeys.length > 0 && args.name === void 0 && args.countMode === void 0) {
28256
+ throw new ConversionArgsError(
28257
+ "--event needs --name: a conversion nobody named is a row labelled with a UUID. (--event with only --count-mode changes how an event already counted is counted.)"
28258
+ );
28021
28259
  }
28022
- if (name !== void 0 && eventKey === void 0 && remove === void 0) {
28260
+ if (args.name !== void 0 && args.eventKeys.length === 0 && args.renameTo === void 0) {
28023
28261
  throw new ConversionArgsError("--name needs --event, naming which event to count.");
28024
28262
  }
28025
- if (eventKey !== void 0 && parseEventKey(eventKey) === null) {
28263
+ if (args.renameFrom !== void 0 && args.renameTo === void 0) {
28264
+ throw new ConversionArgsError('--rename needs --to: `--rename "Lead form" --to "Lead"`.');
28265
+ }
28266
+ if (args.renameTo !== void 0 && args.renameFrom === void 0) {
28026
28267
  throw new ConversionArgsError(
28027
- `"${eventKey}" is not an event key. Run with --candidates and copy one; they look like form:<form>:<step>:<trigger>, submit:<form>, page:<name> or exit:<host>.`
28268
+ '--to needs --rename, naming which outcome to rename: `--rename "Lead form" --to "Lead"`.'
28028
28269
  );
28029
28270
  }
28271
+ }
28272
+ function checkEventKeys(eventKeys) {
28273
+ for (const eventKey of eventKeys) {
28274
+ if (parseEventKey(eventKey) === null) {
28275
+ throw new ConversionArgsError(
28276
+ `"${eventKey}" is not an event key. Run with --candidates (or --flow <slug> for one Form) and copy one; they look like form:<form>:<step>:<trigger>, submit:<form>, page:<name> or exit:<host>.`
28277
+ );
28278
+ }
28279
+ }
28280
+ }
28281
+ function readDays(raw) {
28282
+ if (raw === void 0) return void 0;
28283
+ const days = Number(raw);
28284
+ if (!Number.isFinite(days) || days <= 0) {
28285
+ throw new ConversionArgsError("--days must be a positive number of days.");
28286
+ }
28287
+ return days;
28288
+ }
28289
+ function parseConversionsArgs(raw, argv) {
28290
+ const countMode = onlyOnce(raw["count-mode"], "--count-mode", argv);
28030
28291
  if (countMode !== void 0 && !CONVERSION_COUNT_MODES.includes(countMode)) {
28031
28292
  throw new ConversionArgsError(`--count-mode must be one of ${CONVERSION_COUNT_MODES.join(", ")}.`);
28032
28293
  }
28033
- const days = raw.days === void 0 ? void 0 : Number(raw.days);
28034
- if (days !== void 0 && (!Number.isFinite(days) || days <= 0)) {
28035
- throw new ConversionArgsError("--days must be a positive number of days.");
28294
+ const args = {
28295
+ eventKeys: repeatable("--event", raw.event, argv),
28296
+ removeKeys: repeatable("--remove", raw.remove, argv),
28297
+ name: onlyOnce(raw.name, "--name", argv),
28298
+ countMode,
28299
+ renameFrom: onlyOnce(raw.rename, "--rename", argv),
28300
+ renameTo: onlyOnce(raw.to, "--to", argv),
28301
+ removeName: onlyOnce(raw["remove-name"], "--remove-name", argv),
28302
+ flowSlug: onlyOnce(raw.flow, "--flow", argv),
28303
+ candidates: raw.candidates === true,
28304
+ days: readDays(raw.days)
28305
+ };
28306
+ checkFlagShape(args);
28307
+ checkEventKeys(args.eventKeys);
28308
+ return args;
28309
+ }
28310
+ function existingNames(existing) {
28311
+ return [...new Set(existing.map((row) => row.name))];
28312
+ }
28313
+ function checkNameCollision(name, existing) {
28314
+ const collision = findNameCollision(name, existingNames(existing));
28315
+ if (collision === null) return;
28316
+ throw new ConversionArgsError(
28317
+ `This company already counts "${collision}", and "${name}" would be a second row for the same outcome \u2014 Baker groups by exact name, so the number would be split in two. Use "${collision}" to make them one number, or pick a name that means something different.`
28318
+ );
28319
+ }
28320
+ function under(name, existing) {
28321
+ const rows = existing.filter((row) => row.name === name);
28322
+ if (rows.length === 0) {
28323
+ throw new ConversionArgsError(
28324
+ `Nothing is counted under "${name}". This company counts: ${existingNames(existing).join(", ") || "nothing yet"}.`
28325
+ );
28036
28326
  }
28037
- return { eventKey, name, countMode, remove, candidates: raw.candidates === true, days };
28327
+ return rows;
28038
28328
  }
28039
- function conversionsWrite(args) {
28040
- const set = args.eventKey !== void 0 && args.name !== void 0 ? [{ eventKey: args.eventKey, name: args.name, ...args.countMode ? { countMode: args.countMode } : {} }] : void 0;
28041
- const remove = args.remove !== void 0 ? [args.remove] : void 0;
28042
- if (!set && !remove) return null;
28043
- return { ...set ? { set } : {}, ...remove ? { remove } : {} };
28329
+ function setFromEvents(args, existing) {
28330
+ if (args.eventKeys.length === 0) return [];
28331
+ if (args.name !== void 0) {
28332
+ checkNameCollision(args.name, existing);
28333
+ const name = args.name;
28334
+ return args.eventKeys.map((eventKey) => ({
28335
+ eventKey,
28336
+ name,
28337
+ ...args.countMode ? { countMode: args.countMode } : {}
28338
+ }));
28339
+ }
28340
+ return args.eventKeys.map((eventKey) => {
28341
+ const current = existing.find((row) => row.eventKey === eventKey);
28342
+ if (!current) {
28343
+ throw new ConversionArgsError(
28344
+ `"${eventKey}" is not counted, so there is no count mode to change. Add it with --event "${eventKey}" --name "<what a person calls it>" --count-mode ${args.countMode}.`
28345
+ );
28346
+ }
28347
+ return { eventKey, name: current.name, countMode: args.countMode };
28348
+ });
28349
+ }
28350
+ function setFromRename(args, existing) {
28351
+ if (args.renameFrom === void 0 || args.renameTo === void 0) return [];
28352
+ const renameTo = args.renameTo;
28353
+ checkNameCollision(renameTo, existing);
28354
+ return under(args.renameFrom, existing).map((row) => ({
28355
+ eventKey: row.eventKey,
28356
+ name: renameTo,
28357
+ countMode: row.countMode
28358
+ }));
28359
+ }
28360
+ function conversionsWrite(args, existing) {
28361
+ const set = [...setFromEvents(args, existing), ...setFromRename(args, existing)];
28362
+ const remove = [
28363
+ ...args.removeKeys,
28364
+ ...args.removeName === void 0 ? [] : under(args.removeName, existing).map((row) => row.eventKey)
28365
+ ];
28366
+ if (set.length === 0 && remove.length === 0) return null;
28367
+ return { ...set.length > 0 ? { set } : {}, ...remove.length > 0 ? { remove } : {} };
28044
28368
  }
28045
28369
 
28046
28370
  // src/commands/analytics/conversionHints.ts
@@ -28078,6 +28402,163 @@ function conversionHints(definitions, candidates, askedForCandidates) {
28078
28402
  }
28079
28403
  return hints2;
28080
28404
  }
28405
+ function namingHints(definitions) {
28406
+ const names = [...new Set(definitions.map((row) => row.name))];
28407
+ if (names.length === 0) {
28408
+ return [
28409
+ `Nothing is named yet, so the first name sets the vocabulary for this company. Start from the standard set \u2014 ${STANDARD_CONVERSION_NAMES.map((entry) => entry.name).join(", ")} \u2014 and stay in the company's own language once one exists.`
28410
+ ];
28411
+ }
28412
+ return [
28413
+ `Names in use here: ${names.join(", ")}. Reuse one EXACTLY when another Form produces the same outcome \u2014 that is what makes them one number \u2014 and use \`--rename "<old>" --to "<existing>"\` to merge two that should always have been one.`
28414
+ ];
28415
+ }
28416
+
28417
+ // src/commands/analytics/conversionList.ts
28418
+ function mergeStoredWithCounts(stored, reported) {
28419
+ const counts = new Map(reported.map((row) => [row.name, row]));
28420
+ const groups = /* @__PURE__ */ new Map();
28421
+ for (const row of stored) {
28422
+ const group = groups.get(row.name) ?? [];
28423
+ group.push(row);
28424
+ groups.set(row.name, group);
28425
+ }
28426
+ return [...groups.entries()].map(([name, rows]) => {
28427
+ const modes = new Set(rows.map((row) => row.countMode));
28428
+ const counted = counts.get(name);
28429
+ return {
28430
+ name,
28431
+ eventKeys: rows.map((row) => row.eventKey).sort(),
28432
+ // A real state and usually an editing accident, which is why it is said
28433
+ // out loud rather than resolved to one of them.
28434
+ countMode: modes.size === 1 ? [...modes][0] : "mixed",
28435
+ // Zero rather than absent: an outcome that is counted and has never
28436
+ // fired is the most actionable row on this report, and dropping it
28437
+ // would make it indistinguishable from one nobody has named.
28438
+ conversions: counted?.conversions ?? 0,
28439
+ convertingVisits: counted?.convertingVisits ?? 0,
28440
+ lastSeen: counted?.lastSeen ?? null
28441
+ };
28442
+ }).sort((a, b) => a.name.localeCompare(b.name));
28443
+ }
28444
+
28445
+ // src/commands/analytics/flowConversionKeys.ts
28446
+ function walk(node, acc = []) {
28447
+ if (!node || typeof node !== "object") return acc;
28448
+ const record = node;
28449
+ acc.push(record);
28450
+ if (Array.isArray(record.children)) for (const child of record.children) walk(child, acc);
28451
+ return acc;
28452
+ }
28453
+ function flowConversionCandidates(tree, slug) {
28454
+ const primary = [];
28455
+ const secondary = [];
28456
+ for (const node of walk(tree)) {
28457
+ const id = typeof node.id === "string" ? node.id : null;
28458
+ if (!id) continue;
28459
+ const triggers = flowNodeTriggers(node.nodeData);
28460
+ for (const triggerId of triggers.available) {
28461
+ const candidate = {
28462
+ eventKey: formEventKey(slug, id, triggerId),
28463
+ nodeId: id,
28464
+ nodeName: typeof node.name === "string" ? node.name : id,
28465
+ nodeType: triggers.type,
28466
+ triggerId,
28467
+ isCompletion: triggers.completion === triggerId
28468
+ };
28469
+ (candidate.isCompletion ? primary : secondary).push(candidate);
28470
+ }
28471
+ }
28472
+ return [
28473
+ ...primary,
28474
+ ...secondary,
28475
+ {
28476
+ eventKey: `submit:${slug}`,
28477
+ nodeId: null,
28478
+ nodeName: "the Form ran out of steps",
28479
+ nodeType: null,
28480
+ triggerId: null,
28481
+ // A Form reaching its last node is as primary as an outcome gets for a
28482
+ // Form with no widget in it — but it never fires on one that ends in a
28483
+ // booking widget or a redirect, so it does not lead the list.
28484
+ isCompletion: false
28485
+ }
28486
+ ];
28487
+ }
28488
+ function formKeyRefusal(eventKey, tree, slug) {
28489
+ const parsed = parseEventKey(eventKey);
28490
+ if (!parsed || parsed.family !== "form" || parsed.flowSlug !== slug) return null;
28491
+ const node = walk(tree).find((candidate) => candidate.id === parsed.nodeId);
28492
+ if (!node) {
28493
+ return {
28494
+ message: `"${slug}" has no step with id "${parsed.nodeId}", so this key would be stored and count zero forever.`,
28495
+ fix: `Steps in this Form: ${walk(tree).map((n) => `${String(n.id)} (${String(n.name ?? "unnamed")})`).join(", ")}. \`baker analytics conversions --flow ${slug}\` prints each one already spelled as an event key.`
28496
+ };
28497
+ }
28498
+ const triggers = flowNodeTriggers(node.nodeData);
28499
+ if (!triggers.known || triggers.available.includes(parsed.triggerId)) return null;
28500
+ return {
28501
+ message: `Step "${String(node.name ?? node.id)}" never raises \`${parsed.triggerId}\`, so counting it would score zero however many people reach that step.`,
28502
+ fix: triggers.available.length > 0 ? `It raises: ${triggers.available.join(", ")}.` : triggers.why ?? "That step raises nothing a conversion could be counted on."
28503
+ };
28504
+ }
28505
+ function joinFlowConversions(candidates, definitions) {
28506
+ const byKey = /* @__PURE__ */ new Map();
28507
+ for (const definition of definitions) {
28508
+ for (const key of definition.eventKeys) byKey.set(key, definition.name);
28509
+ }
28510
+ return candidates.map((candidate) => ({ ...candidate, countedAs: byKey.get(candidate.eventKey) ?? null }));
28511
+ }
28512
+ function flowConversionHints(slug, rows, noConversionReason) {
28513
+ const counted = rows.filter((row) => row.countedAs !== null);
28514
+ if (counted.length > 0) {
28515
+ return [
28516
+ `Baker counts ${counted.length} of this Form's endings: ${counted.map((row) => `${row.countedAs} (${row.nodeName})`).join(
28517
+ ", "
28518
+ )}. Every other ending here scores nothing \u2014 count another with \`--event <key> --name <outcome>\`, reusing the SAME name to make two Forms one number.`
28519
+ ];
28520
+ }
28521
+ if (noConversionReason) {
28522
+ return [
28523
+ `Nothing here is counted, and that is on purpose: this Form declares no outcome \u2014 "${noConversionReason}". Leave it alone unless that has stopped being true.`
28524
+ ];
28525
+ }
28526
+ const deepestFirst = [...rows].reverse();
28527
+ const suggestion = deepestFirst.find((row) => row.isCompletion && row.nodeType !== "text") ?? deepestFirst.find((row) => row.isCompletion) ?? rows[rows.length - 1];
28528
+ return [
28529
+ `Nothing on "${slug}" is counted, so it converts nobody on every report however many people finish it \u2014 Baker never guesses which ending is the point of a Form.`,
28530
+ ...suggestion ? [
28531
+ `Name its outcome: \`baker analytics conversions --event "${suggestion.eventKey}" --name "<what a person calls it>"\` (that key is "${suggestion.nodeName}"). It applies at once and rescores the whole history; nothing is staged and there is nothing to publish.`
28532
+ ] : [],
28533
+ `If this Form has no outcome of its own \u2014 it hands the visitor to a shop, a portal or a funnel that converts them there \u2014 say so instead, with \`noConversion: { "reason": "\u2026" }\` on its first step.`
28534
+ ];
28535
+ }
28536
+ function flowCoverage(forms, definitions) {
28537
+ return forms.map(({ slug, tree }) => {
28538
+ const rows = joinFlowConversions(flowConversionCandidates(tree, slug), definitions);
28539
+ const declared = tree?.noConversion?.reason;
28540
+ const seen = /* @__PURE__ */ new Set();
28541
+ return {
28542
+ slug,
28543
+ counted: rows.filter((row) => row.countedAs !== null).filter((row) => {
28544
+ const key = `${row.countedAs} ${row.nodeName}`;
28545
+ if (seen.has(key)) return false;
28546
+ seen.add(key);
28547
+ return true;
28548
+ }).map((row) => ({ name: String(row.countedAs), step: row.nodeName })),
28549
+ noConversion: typeof declared === "string" && declared.trim() !== "" ? declared.trim() : null
28550
+ };
28551
+ });
28552
+ }
28553
+ function flowCoverageHints(coverage) {
28554
+ const unset = coverage.filter((row) => row.counted.length === 0 && row.noConversion === null);
28555
+ if (unset.length === 0) return [];
28556
+ return [
28557
+ `${unset.length} of ${coverage.length} Form(s) here count nothing, so every report shows them converting nobody: ${unset.map((row) => row.slug).join(
28558
+ ", "
28559
+ )}. Read each one's endings with \`baker analytics conversions --flow <slug>\` and name the outcome, or declare on its first step why it has none.`
28560
+ ];
28561
+ }
28081
28562
 
28082
28563
  // src/commands/analytics/hints.ts
28083
28564
  var SEVERE_STEP_DROP = 0.6;
@@ -28960,26 +29441,93 @@ Examples:
28960
29441
  }
28961
29442
  });
28962
29443
  })();
29444
+ function localFormKeyRefusal(eventKey) {
29445
+ const parsed = parseEventKey(eventKey);
29446
+ if (!parsed || parsed.family !== "form") return null;
29447
+ if (!flowExists(parsed.flowSlug)) return null;
29448
+ return formKeyRefusal(eventKey, readFlowTree(parsed.flowSlug), parsed.flowSlug);
29449
+ }
29450
+ function refuseUnmatchableKey(eventKey) {
29451
+ const refusal = eventKey === void 0 ? null : localFormKeyRefusal(eventKey);
29452
+ if (!refusal) return;
29453
+ writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message: refusal.message, fix: refusal.fix } });
29454
+ process.exit(1);
29455
+ }
29456
+ function readWorkspaceFlows() {
29457
+ return listFlowSlugs().flatMap((slug) => {
29458
+ try {
29459
+ return [{ slug, tree: JSON.parse(readFileSync12(flowDataPath(slug), "utf-8")) }];
29460
+ } catch {
29461
+ return [];
29462
+ }
29463
+ });
29464
+ }
29465
+ function readFlowConversions2(slug, definitions) {
29466
+ const tree = readFlowTree(slug);
29467
+ const declared = tree.noConversion;
29468
+ const reason = declared && typeof declared === "object" && typeof declared.reason === "string" ? String(declared.reason) : null;
29469
+ return {
29470
+ slug,
29471
+ noConversionReason: reason,
29472
+ endings: joinFlowConversions(flowConversionCandidates(tree, slug), definitions)
29473
+ };
29474
+ }
29475
+ function conversionsEnvelope(args, definitions, candidates) {
29476
+ const flow = args.flowSlug === void 0 ? null : readFlowConversions2(args.flowSlug, definitions);
29477
+ const forms = flow === null ? readWorkspaceFlows() : [];
29478
+ const coverage = flowCoverage(forms, definitions);
29479
+ const hints2 = [
29480
+ ...flow ? flowConversionHints(flow.slug, flow.endings, flow.noConversionReason) : [],
29481
+ ...flowCoverageHints(coverage),
29482
+ ...conversionHints(definitions, candidates, args.candidates),
29483
+ ...namingHints(definitions)
29484
+ ];
29485
+ return {
29486
+ ok: true,
29487
+ data: {
29488
+ definitions,
29489
+ ...flow ? { flow } : {},
29490
+ ...coverage.length > 0 ? { flows: coverage } : {},
29491
+ ...args.candidates ? { candidates } : {}
29492
+ },
29493
+ ...hints2.length > 0 ? { hints: hints2 } : {}
29494
+ };
29495
+ }
28963
29496
  var conversionsCommand3 = (() => {
28964
29497
  const args = {
28965
29498
  event: {
28966
29499
  type: "string",
28967
- description: "The event to start counting, from the `candidates` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. Applies immediately and scores the WHOLE history, not just from now on \u2014 so counting the right event today also corrects last month's reports",
29500
+ description: "The event to start counting, from the `candidates` or `--flow` list: form:<form>:<step>:<trigger>, submit:<form>, page:<name>, or exit:<host>. Requires --name. REPEATABLE \u2014 pass it several times with one --name to count all of them as that single outcome, which is the whole point of the name. Applies immediately and scores the WHOLE history, not just from now on, so counting the right event today also corrects last month's reports",
28968
29501
  required: false
28969
29502
  },
28970
29503
  name: {
28971
29504
  type: "string",
28972
- description: "What to call this outcome on every report \u2014 'Booked a call', 'Quote requested'. Reuse the SAME name on two events and they become one row and one number, which is how a call booked on three different Forms reads as one outcome rather than three",
29505
+ description: "What to call this outcome on every report \u2014 'Booked a call', 'Quote requested'. Baker groups by EXACT name: reuse one this company already counts and the two become one row and one number, which is how a call booked on three Forms reads as one outcome. Read the existing names first (run with no flags) and reuse one before inventing another; a name that only differs by case, accent, punctuation or a plural is refused, because it would split the number in two",
29506
+ required: false
29507
+ },
29508
+ rename: {
29509
+ type: "string",
29510
+ description: "An outcome's current name. With --to, renames every event counted under it. Renaming ONTO a name that already exists MERGES them into one row and one number \u2014 the way to fix a company whose Forms each invented their own word for the same outcome",
29511
+ required: false
29512
+ },
29513
+ to: {
29514
+ type: "string",
29515
+ description: "The new name for --rename",
29516
+ required: false
29517
+ },
29518
+ "remove-name": {
29519
+ type: "string",
29520
+ description: "Stop counting an outcome entirely, by name \u2014 removes every event counted under it. Use --remove for a single event key instead",
28973
29521
  required: false
28974
29522
  },
28975
29523
  "count-mode": {
28976
29524
  type: "string",
28977
- description: "once_per_visit (default) or every_time. Ask whether doing it twice in one visit is one outcome or two: a booking clicked twice is one booking, a guide downloaded twice is two downloads. Getting this wrong inflates the rate every budget is set by",
29525
+ description: "once_per_visit (default) or every_time. Ask whether doing it twice in one visit is one outcome or two: a booking clicked twice is one booking, a guide downloaded twice is two downloads. Getting this wrong inflates the rate every budget is set by. Pass it with --event and no --name to change the mode of something already counted, keeping its name",
28978
29526
  required: false
28979
29527
  },
28980
29528
  remove: {
28981
29529
  type: "string",
28982
- description: "An event key to stop counting. Removes it from the history too, for the same reason adding it fills the history in",
29530
+ description: "An event key to stop counting. REPEATABLE. Removes it from the history too, for the same reason adding it fills the history in",
28983
29531
  required: false
28984
29532
  },
28985
29533
  candidates: {
@@ -28987,6 +29535,11 @@ var conversionsCommand3 = (() => {
28987
29535
  description: "Also list every event these pages actually produced, ranked by volume, with whether it is already counted. This is where an event key comes from \u2014 do not invent one. Off by default because a busy company produces hundreds of rows",
28988
29536
  required: false
28989
29537
  },
29538
+ flow: {
29539
+ type: "string",
29540
+ description: "A Form slug. Lists that Form's own endings already spelled as event keys, each with the name Baker counts it under or nothing \u2014 read off the Form's file, so it works on a Form built minutes ago that has never been visited, which is exactly when --candidates is empty. Use it whenever you create or edit a Form: it is the only way to see that the Form counts nothing before it is published counting nothing",
29541
+ required: false
29542
+ },
28990
29543
  days: {
28991
29544
  type: "string",
28992
29545
  description: "Lookback for the counts and for --candidates. Default 30",
@@ -28995,34 +29548,33 @@ var conversionsCommand3 = (() => {
28995
29548
  };
28996
29549
  registerSchema({
28997
29550
  command: "analytics.conversions",
28998
- description: "Read or change what this company counts as a conversion",
29551
+ description: "Read or change what this company counts as a conversion. Also `--flow <slug>` for one Form's own endings as event keys, whether or not any of them has fired.",
28999
29552
  args
29000
29553
  });
29001
29554
  return defineCommand95({
29002
29555
  meta: {
29003
29556
  name: "conversions",
29004
- description: "What this company counts as a conversion, and what each one produced. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. It applies IMMEDIATELY and retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers the moment you name the right event. Nothing here is staged and publishing is not involved. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one."
29557
+ description: "What this company counts as a conversion, and what each one produced. Run with no flags first: an empty `definitions` list means every conversion number in every other report is zero \u2014 not because nobody converted, but because nothing is marked as converting, and Baker never guesses. Add `--candidates` to see everything these pages actually do (every Form step, every data-baker-* event, every outbound destination) with counts and whether it is already counted; that list is where an event key comes from, so never invent one. Then `--event <key> --name 'Booked a call'` to count it. It applies IMMEDIATELY and retroactively \u2014 the whole history is rescored, so a company that has been running for a month gets a month of correct numbers the moment you name the right event. Nothing here is staged and publishing is not involved. Two events given the SAME --name are one row and one number. `--remove <key>` stops counting one. `--flow <slug>` answers the other direction \u2014 what one Form counts, with every ending it has already spelled as an event key, read from the Form itself so it works before a single visitor has seen it. Run it whenever you create or edit a Form: a Form nobody named an outcome for is published converting nobody, and no report says so."
29005
29558
  },
29006
29559
  args,
29007
29560
  run: async ({ args: raw }) => {
29008
29561
  try {
29009
29562
  const args2 = parseConversionsArgs(raw, process.argv);
29010
- const write2 = conversionsWrite(args2);
29011
- if (write2) {
29012
- await apiPost("/api/analytics/conversions", write2);
29013
- }
29563
+ for (const eventKey of args2.eventKeys) refuseUnmatchableKey(eventKey);
29564
+ const before = await apiPost("/api/analytics/conversions", {});
29565
+ const write2 = conversionsWrite(args2, before.data.definitions);
29566
+ const stored = write2 ? (await apiPost("/api/analytics/conversions", write2)).data.definitions : before.data.definitions;
29014
29567
  const report = await apiPost("/api/analytics/query", {
29015
29568
  preset: "conversions",
29016
29569
  days: args2.days
29017
29570
  });
29018
- const definitions = report.data.conversions ?? [];
29019
- const candidates = report.data.eventCatalog ?? [];
29020
- const hints2 = conversionHints(definitions, candidates, args2.candidates);
29021
- writeJsonEnvelope({
29022
- ok: true,
29023
- data: { definitions, ...args2.candidates ? { candidates } : {} },
29024
- ...hints2.length > 0 ? { hints: hints2 } : {}
29025
- });
29571
+ writeJsonEnvelope(
29572
+ conversionsEnvelope(
29573
+ args2,
29574
+ mergeStoredWithCounts(stored, report.data.conversions ?? []),
29575
+ report.data.eventCatalog ?? []
29576
+ )
29577
+ );
29026
29578
  } catch (err) {
29027
29579
  if (err instanceof ConversionArgsError) {
29028
29580
  handleError(new ApiError("VALIDATION_ERROR", err.message));
@@ -29460,11 +30012,11 @@ function unwrap(response) {
29460
30012
  }
29461
30013
  return response.data;
29462
30014
  }
29463
- async function readAvatars(path42, params) {
29464
- return unwrap(await apiGet(path42, params));
30015
+ async function readAvatars(path44, params) {
30016
+ return unwrap(await apiGet(path44, params));
29465
30017
  }
29466
- async function writeAvatars(path42, body) {
29467
- return unwrap(await apiPost(path42, body));
30018
+ async function writeAvatars(path44, body) {
30019
+ return unwrap(await apiPost(path44, body));
29468
30020
  }
29469
30021
 
29470
30022
  // src/commands/avatars/create.ts
@@ -30298,12 +30850,12 @@ function missingFontFiles(urls, available) {
30298
30850
  function planFontAdoption(sources, families) {
30299
30851
  const wanted = new Map(families.map((family) => [normalizeFamily(family), family]));
30300
30852
  const byFamily = /* @__PURE__ */ new Map();
30301
- for (const { path: path42, source } of sources) {
30302
- const dir = posix.dirname(path42);
30853
+ for (const { path: path44, source } of sources) {
30854
+ const dir = posix.dirname(path44);
30303
30855
  for (const face of declaredFontFaces(source)) {
30304
30856
  if (!wanted.has(face.family)) continue;
30305
30857
  const perFile = byFamily.get(face.family) ?? /* @__PURE__ */ new Map();
30306
- perFile.set(path42, [...perFile.get(path42) ?? [], rebaseFontFaceSrc(face.block, dir)]);
30858
+ perFile.set(path44, [...perFile.get(path44) ?? [], rebaseFontFaceSrc(face.block, dir)]);
30307
30859
  byFamily.set(face.family, perFile);
30308
30860
  }
30309
30861
  }
@@ -31324,10 +31876,10 @@ import path15 from "path";
31324
31876
  import { defineCommand as defineCommand107 } from "citty";
31325
31877
 
31326
31878
  // src/commands/canvas/normalize-paths.ts
31327
- import { existsSync as existsSync3, realpathSync } from "fs";
31879
+ import { existsSync as existsSync4, realpathSync } from "fs";
31328
31880
  import { writeFile as writeFile2 } from "fs/promises";
31329
31881
  import path5 from "path";
31330
- function findWorkspaceRoot(startDir, exists2 = existsSync3, maxDepth = 12) {
31882
+ function findWorkspaceRoot(startDir, exists2 = existsSync4, maxDepth = 12) {
31331
31883
  let dir = path5.resolve(startDir);
31332
31884
  for (let i = 0; i < maxDepth; i++) {
31333
31885
  if (exists2(path5.join(dir, "package.json"))) return dir;
@@ -35277,10 +35829,10 @@ function runDirsToPrune(entries, keep, currentRunId) {
35277
35829
  return runs.slice(0, Math.max(0, runs.length - keep));
35278
35830
  }
35279
35831
  async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
35280
- const { readdir: readdir12 } = await import("fs/promises");
35832
+ const { readdir: readdir14 } = await import("fs/promises");
35281
35833
  let entries;
35282
35834
  try {
35283
- entries = await readdir12(outputsDir);
35835
+ entries = await readdir14(outputsDir);
35284
35836
  } catch {
35285
35837
  return;
35286
35838
  }
@@ -36972,7 +37524,7 @@ function routeVideoModel(input) {
36972
37524
  import { execFile as execFile2 } from "child_process";
36973
37525
  import { mkdtemp, readdir as readdir7, readFile as readFile14, rm as rm5 } from "fs/promises";
36974
37526
  import { tmpdir } from "os";
36975
- import { join as join2 } from "path";
37527
+ import { join as join3 } from "path";
36976
37528
  import { promisify as promisify2 } from "util";
36977
37529
  var execFileAsync2 = promisify2(execFile2);
36978
37530
  var PYSCENEDETECT_THRESHOLD = 18;
@@ -37025,7 +37577,7 @@ function parsePySceneDetectCsvCuts(csv) {
37025
37577
  return [...new Set(cuts)].sort((a, b) => a - b);
37026
37578
  }
37027
37579
  async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs) {
37028
- const outDir = await mkdtemp(join2(tmpdir(), "baker-scenedetect-"));
37580
+ const outDir = await mkdtemp(join3(tmpdir(), "baker-scenedetect-"));
37029
37581
  try {
37030
37582
  await execFileAsync2(
37031
37583
  "scenedetect",
@@ -37046,7 +37598,7 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
37046
37598
  );
37047
37599
  const csvName = (await readdir7(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
37048
37600
  if (!csvName) return [];
37049
- return parsePySceneDetectCsvCuts(await readFile14(join2(outDir, csvName), "utf-8"));
37601
+ return parsePySceneDetectCsvCuts(await readFile14(join3(outDir, csvName), "utf-8"));
37050
37602
  } finally {
37051
37603
  await rm5(outDir, { recursive: true, force: true });
37052
37604
  }
@@ -37070,9 +37622,9 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
37070
37622
  }
37071
37623
 
37072
37624
  // src/commands/canvas/composition-path.ts
37073
- import { existsSync as existsSync4 } from "fs";
37625
+ import { existsSync as existsSync5 } from "fs";
37074
37626
  import path21 from "path";
37075
- function resolveShippedCanvasDir(name, startDir, exists2 = existsSync4, maxDepth = 8) {
37627
+ function resolveShippedCanvasDir(name, startDir, exists2 = existsSync5, maxDepth = 8) {
37076
37628
  const rel = path21.join("canvas", name);
37077
37629
  let dir = startDir;
37078
37630
  for (let i = 0; i < maxDepth; i++) {
@@ -38457,6 +39009,10 @@ Full guide: __tooling__/docs/tools/baker/creatives.md`
38457
39009
  // src/commands/experiment/index.ts
38458
39010
  import { defineCommand as defineCommand119 } from "citty";
38459
39011
 
39012
+ // src/commands/experiment/composition.ts
39013
+ import { readdir as readdir9, readFile as readFile21 } from "fs/promises";
39014
+ import path27 from "path";
39015
+
38460
39016
  // src/commands/landing/import-graph.ts
38461
39017
  import { readdir as readdir8, readFile as readFile20, stat as stat4 } from "fs/promises";
38462
39018
  import path26 from "path";
@@ -38566,10 +39122,51 @@ async function readComposition(root, controlSlug, variantSlug2) {
38566
39122
  return void 0;
38567
39123
  }
38568
39124
  }
39125
+ var FLOW_REFERENCE_RES = [
39126
+ /["'][^"']*flow-engine\/flows\/([A-Za-z0-9._-]+)/g,
39127
+ /data-flow-form\s*=\s*["']([^"']+)["']/g,
39128
+ /flowName\s*=\s*["']([^"']+)["']/g
39129
+ ];
39130
+ function formsReferenced(texts, knownFlows2) {
39131
+ const found = /* @__PURE__ */ new Set();
39132
+ for (const text2 of texts) {
39133
+ for (const re2 of FLOW_REFERENCE_RES) {
39134
+ for (const match of text2.matchAll(re2)) {
39135
+ const flow = match[1];
39136
+ if (flow && knownFlows2.has(flow)) found.add(flow);
39137
+ }
39138
+ }
39139
+ }
39140
+ return [...found].sort();
39141
+ }
39142
+ function formsOnlyIn(these, those) {
39143
+ const other = new Set(those);
39144
+ return these.filter((flow) => !other.has(flow));
39145
+ }
39146
+ async function knownFlows(root) {
39147
+ try {
39148
+ const entries = await readdir9(path27.join(root, "src/lib/flow-engine/flows"), { withFileTypes: true });
39149
+ return new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
39150
+ } catch {
39151
+ return /* @__PURE__ */ new Set();
39152
+ }
39153
+ }
39154
+ async function readForms(root, slug) {
39155
+ try {
39156
+ const [files, flows] = await Promise.all([landingGraphFiles(root, slug), knownFlows(root)]);
39157
+ if (flows.size === 0) return [];
39158
+ const texts = await Promise.all(
39159
+ [...files].filter((file) => /\.(astro|ts|tsx|js|mjs)$/.test(file)).map((file) => readFile21(path27.join(root, file), "utf8"))
39160
+ );
39161
+ return formsReferenced(texts, flows);
39162
+ } catch {
39163
+ return [];
39164
+ }
39165
+ }
38569
39166
 
38570
39167
  // src/commands/experiment/fold.ts
38571
- import { cp as cp2, mkdir as mkdir8, readdir as readdir9, readFile as readFile21, rm as rm7, stat as stat5, writeFile as writeFile11 } from "fs/promises";
38572
- import path28 from "path";
39168
+ import { cp as cp2, mkdir as mkdir8, readdir as readdir10, readFile as readFile22, rm as rm7, stat as stat5, writeFile as writeFile11 } from "fs/promises";
39169
+ import path29 from "path";
38573
39170
 
38574
39171
  // ../api/src/experiments/variants.ts
38575
39172
  var VARIANT_SEPARATOR = "--";
@@ -38594,21 +39191,21 @@ function nextVariantNumber(page, existingSlugs, archived) {
38594
39191
  }
38595
39192
 
38596
39193
  // src/commands/landing/repoint.ts
38597
- import path27 from "path";
39194
+ import path28 from "path";
38598
39195
  var QUOTED_RELATIVE = /(['"])(\.\.?\/[^'"]*)\1/g;
38599
39196
  function isInside3(abs, dir) {
38600
- return abs === dir || abs.startsWith(dir + path27.sep);
39197
+ return abs === dir || abs.startsWith(dir + path28.sep);
38601
39198
  }
38602
39199
  function toSpecifier(from, to) {
38603
- const rel = path27.relative(from, to).split(path27.sep).join("/");
39200
+ const rel = path28.relative(from, to).split(path28.sep).join("/");
38604
39201
  return rel.startsWith(".") ? rel : `./${rel}`;
38605
39202
  }
38606
39203
  function repointSpecifier(spec, opts) {
38607
39204
  if (!spec.startsWith(".")) return spec;
38608
- const abs = path27.resolve(opts.fromDir, spec);
39205
+ const abs = path28.resolve(opts.fromDir, spec);
38609
39206
  if (opts.forkedTargets?.has(abs)) {
38610
- const withinControl = path27.relative(opts.controlDir, abs);
38611
- return toSpecifier(opts.toDir, path27.resolve(opts.variantDir, withinControl));
39207
+ const withinControl = path28.relative(opts.controlDir, abs);
39208
+ return toSpecifier(opts.toDir, path28.resolve(opts.variantDir, withinControl));
38612
39209
  }
38613
39210
  if (!isInside3(abs, opts.controlDir)) return spec;
38614
39211
  return toSpecifier(opts.toDir, abs);
@@ -38620,9 +39217,9 @@ function repointFile(text2, opts) {
38620
39217
  }
38621
39218
  function repointMoved(text2, opts) {
38622
39219
  return text2.replace(QUOTED_RELATIVE, (_match, quote, spec) => {
38623
- const abs = path27.resolve(opts.fromDir, spec);
38624
- const within = path27.relative(opts.movedFrom, abs);
38625
- const target = within.startsWith("..") || path27.isAbsolute(within) ? abs : path27.resolve(opts.movedTo, within);
39220
+ const abs = path28.resolve(opts.fromDir, spec);
39221
+ const within = path28.relative(opts.movedFrom, abs);
39222
+ const target = within.startsWith("..") || path28.isAbsolute(within) ? abs : path28.resolve(opts.movedTo, within);
38626
39223
  return `${quote}${toSpecifier(opts.toDir, target)}${quote}`;
38627
39224
  });
38628
39225
  }
@@ -38631,7 +39228,7 @@ function repointMoved(text2, opts) {
38631
39228
  var ARCHIVE_ROOT = "src/_variants";
38632
39229
  var QUOTED_RELATIVE2 = /(['"])(\.\.?\/[^'"]*)\1/g;
38633
39230
  function posix2(p) {
38634
- return p.split(path28.sep).join("/");
39231
+ return p.split(path29.sep).join("/");
38635
39232
  }
38636
39233
  async function isDir(p) {
38637
39234
  try {
@@ -38643,46 +39240,46 @@ async function isDir(p) {
38643
39240
  async function filesUnder(dir, prefix = "") {
38644
39241
  let entries;
38645
39242
  try {
38646
- entries = await readdir9(dir, { withFileTypes: true });
39243
+ entries = await readdir10(dir, { withFileTypes: true });
38647
39244
  } catch {
38648
39245
  return [];
38649
39246
  }
38650
39247
  const out = [];
38651
39248
  for (const entry of entries) {
38652
39249
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
38653
- if (entry.isDirectory()) out.push(...await filesUnder(path28.join(dir, entry.name), rel));
39250
+ if (entry.isDirectory()) out.push(...await filesUnder(path29.join(dir, entry.name), rel));
38654
39251
  else out.push(rel);
38655
39252
  }
38656
39253
  return out.sort();
38657
39254
  }
38658
39255
  var TEXT_EXT = /\.(astro|ts|tsx|js|jsx|mjs|cjs|vue|svelte|md|json|css)$/;
38659
39256
  function archiveRelPath(abs, base) {
38660
- if (!abs.startsWith(base + path28.sep)) return null;
38661
- return posix2(path28.relative(base, abs));
39257
+ if (!abs.startsWith(base + path29.sep)) return null;
39258
+ return posix2(path29.relative(base, abs));
38662
39259
  }
38663
39260
  async function archiveArm(opts) {
38664
39261
  const { root, armDir, pageDir, archiveDir, armSlug } = opts;
38665
39262
  const targets = await archiveContents({ root, armDir, pageDir, armSlug });
38666
- const archivedBySource = new Map([...targets].map(([rel, abs]) => [abs, path28.join(archiveDir, rel)]));
39263
+ const archivedBySource = new Map([...targets].map(([rel, abs]) => [abs, path29.join(archiveDir, rel)]));
38667
39264
  const written = [];
38668
39265
  for (const [rel, abs] of [...targets].sort()) {
38669
- const dest = path28.join(archiveDir, rel);
38670
- await mkdir8(path28.dirname(dest), { recursive: true });
39266
+ const dest = path29.join(archiveDir, rel);
39267
+ await mkdir8(path29.dirname(dest), { recursive: true });
38671
39268
  if (!TEXT_EXT.test(rel)) {
38672
39269
  await cp2(abs, dest);
38673
39270
  } else {
38674
- const text2 = await readFile21(abs, "utf8");
39271
+ const text2 = await readFile22(abs, "utf8");
38675
39272
  await writeFile11(dest, repointIntoArchive(text2, abs, dest, archivedBySource), "utf8");
38676
39273
  }
38677
- written.push(posix2(path28.relative(root, dest)));
39274
+ written.push(posix2(path29.relative(root, dest)));
38678
39275
  }
38679
39276
  return written;
38680
39277
  }
38681
39278
  async function archiveContents(opts) {
38682
39279
  const { root, armDir, pageDir, armSlug } = opts;
38683
39280
  const graph = await landingGraphFiles(root, armSlug);
38684
- const reachable = new Set([...graph].map((rel) => path28.resolve(root, rel)));
38685
- for (const rel of await filesUnder(armDir)) reachable.add(path28.join(armDir, rel));
39281
+ const reachable = new Set([...graph].map((rel) => path29.resolve(root, rel)));
39282
+ for (const rel of await filesUnder(armDir)) reachable.add(path29.join(armDir, rel));
38686
39283
  const sorted = [...reachable].sort();
38687
39284
  const targets = /* @__PURE__ */ new Map();
38688
39285
  for (const base of [pageDir, armDir]) {
@@ -38696,9 +39293,9 @@ async function archiveContents(opts) {
38696
39293
  }
38697
39294
  function repointIntoArchive(text2, sourceAbs, destAbs, archivedBySource) {
38698
39295
  return text2.replace(QUOTED_RELATIVE2, (match, quote, spec) => {
38699
- const resolved = resolveWithExtensions(path28.resolve(path28.dirname(sourceAbs), spec), archivedBySource);
39296
+ const resolved = resolveWithExtensions(path29.resolve(path29.dirname(sourceAbs), spec), archivedBySource);
38700
39297
  if (resolved === null) return match;
38701
- const rel = posix2(path28.relative(path28.dirname(destAbs), resolved));
39298
+ const rel = posix2(path29.relative(path29.dirname(destAbs), resolved));
38702
39299
  return `${quote}${rel.startsWith(".") ? rel : `./${rel}`}${quote}`;
38703
39300
  });
38704
39301
  }
@@ -38714,25 +39311,25 @@ async function promoteVariantFiles(opts) {
38714
39311
  const written = [];
38715
39312
  for (const rel of await filesUnder(variantDir)) {
38716
39313
  if (rel === "_definition.md") continue;
38717
- const from = path28.join(variantDir, rel);
38718
- const to = path28.join(pageDir, rel);
38719
- await mkdir8(path28.dirname(to), { recursive: true });
39314
+ const from = path29.join(variantDir, rel);
39315
+ const to = path29.join(pageDir, rel);
39316
+ await mkdir8(path29.dirname(to), { recursive: true });
38720
39317
  if (!TEXT_EXT.test(rel)) {
38721
39318
  await cp2(from, to);
38722
39319
  } else {
38723
- const text2 = await readFile21(from, "utf8");
39320
+ const text2 = await readFile22(from, "utf8");
38724
39321
  await writeFile11(
38725
39322
  to,
38726
39323
  repointMoved(text2, {
38727
- fromDir: path28.dirname(from),
38728
- toDir: path28.dirname(to),
39324
+ fromDir: path29.dirname(from),
39325
+ toDir: path29.dirname(to),
38729
39326
  movedFrom: variantDir,
38730
39327
  movedTo: pageDir
38731
39328
  }),
38732
39329
  "utf8"
38733
39330
  );
38734
39331
  }
38735
- written.push(posix2(path28.relative(root, to)));
39332
+ written.push(posix2(path29.relative(root, to)));
38736
39333
  }
38737
39334
  return written;
38738
39335
  }
@@ -38756,8 +39353,8 @@ function archiveRecord(target, variantNumber) {
38756
39353
  `;
38757
39354
  }
38758
39355
  async function foldExperiment(root, target) {
38759
- const pageDir = path28.resolve(root, "src/pages", target.landingSlug);
38760
- const variantDir = path28.resolve(root, "src/pages", target.variantSlug);
39356
+ const pageDir = path29.resolve(root, "src/pages", target.landingSlug);
39357
+ const variantDir = path29.resolve(root, "src/pages", target.variantSlug);
38761
39358
  const variantNumber = variantNumberOf(target.variantSlug);
38762
39359
  if (variantNumber === null) {
38763
39360
  return {
@@ -38771,7 +39368,7 @@ async function foldExperiment(root, target) {
38771
39368
  return { error: `src/pages/${target.variantSlug}/ is already gone \u2014 this test looks folded in already` };
38772
39369
  }
38773
39370
  const archiveKey = target.landingInternalId ?? target.landingSlug;
38774
- const archiveDir = path28.resolve(root, ARCHIVE_ROOT, archiveKey, String(variantNumber));
39371
+ const archiveDir = path29.resolve(root, ARCHIVE_ROOT, archiveKey, String(variantNumber));
38775
39372
  await rm7(archiveDir, { recursive: true, force: true });
38776
39373
  await mkdir8(archiveDir, { recursive: true });
38777
39374
  const written = await archiveArm({
@@ -38781,8 +39378,8 @@ async function foldExperiment(root, target) {
38781
39378
  archiveDir,
38782
39379
  armSlug: target.survivor === "variant" ? target.landingSlug : target.variantSlug
38783
39380
  });
38784
- await writeFile11(path28.join(archiveDir, "_variant.json"), archiveRecord(target, variantNumber), "utf8");
38785
- written.push(posix2(path28.join(ARCHIVE_ROOT, archiveKey, String(variantNumber), "_variant.json")));
39381
+ await writeFile11(path29.join(archiveDir, "_variant.json"), archiveRecord(target, variantNumber), "utf8");
39382
+ written.push(posix2(path29.join(ARCHIVE_ROOT, archiveKey, String(variantNumber), "_variant.json")));
38786
39383
  if (target.survivor === "variant") {
38787
39384
  written.push(...await promoteVariantFiles({ root, pageDir, variantDir }));
38788
39385
  }
@@ -38790,7 +39387,7 @@ async function foldExperiment(root, target) {
38790
39387
  return {
38791
39388
  experimentId: target.experimentId,
38792
39389
  page: target.landingSlug,
38793
- archivedTo: posix2(path28.join(ARCHIVE_ROOT, archiveKey, String(variantNumber))),
39390
+ archivedTo: posix2(path29.join(ARCHIVE_ROOT, archiveKey, String(variantNumber))),
38794
39391
  written,
38795
39392
  removed: [`src/pages/${target.variantSlug}`]
38796
39393
  };
@@ -38801,7 +39398,7 @@ var LIVE_HINT = {
38801
39398
  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)}`,
38802
39399
  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.`,
38803
39400
  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.`,
38804
- stopped_early_harmful: (row) => `${row.experimentId} is doing damage and is STILL being shown to half the visitors. Run \`baker experiment finish --id ${row.experimentId} --keep original\` now.`,
39401
+ 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.`,
38805
39402
  // Unreachable on a live test — only ending one produces it. The Record is
38806
39403
  // exhaustive by type so a verdict added upstream fails to compile here rather
38807
39404
  // than printing nothing.
@@ -38820,8 +39417,18 @@ function describeWait(row) {
38820
39417
  function pausedNote(row) {
38821
39418
  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. ` : "";
38822
39419
  }
39420
+ function unlearnedHint(experiments) {
39421
+ const unlearned = experiments.filter(
39422
+ (row) => row.status === "finished" && row.learning === null && row.verdict !== "inconclusive"
39423
+ );
39424
+ if (unlearned.length === 0) return null;
39425
+ const ids = unlearned.map((row) => row.experimentId).join(", ");
39426
+ 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.`;
39427
+ }
38823
39428
  function buildStatusHints(experiments) {
38824
39429
  const hints2 = experiments.filter((row) => row.status !== "finished").map((row) => pausedNote(row) + LIVE_HINT[row.verdict](row));
39430
+ const unlearned = unlearnedHint(experiments);
39431
+ if (unlearned) hints2.push(unlearned);
38825
39432
  if (experiments.length === 0) {
38826
39433
  hints2.push(
38827
39434
  "No tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page has enough traffic for one before you build the alternative."
@@ -38842,24 +39449,50 @@ function horizonsHint(plan) {
38842
39449
  }
38843
39450
  function conversionsHint(plan) {
38844
39451
  if (plan.conversions.length <= 1) return null;
38845
- const others = plan.conversions.filter((entry) => entry.name !== plan.goal);
38846
- return `This page's visitors also went on to: ${others.map((entry) => `\u201C${entry.name}\u201D (${entry.convertingVisitors})`).join(", ")}. They are reported beside the goal on every result, and decide nothing.`;
39452
+ const others = plan.conversions.filter((entry) => entry.eventKey !== plan.goal);
39453
+ return `This page's visitors also went on to: ${others.map((entry) => `\`${entry.eventKey}\`${entry.name ? ` (\u201C${entry.name}\u201D)` : ""} \u2014 ${entry.convertingVisitors}`).join(
39454
+ ", "
39455
+ )}. Any of them can be the goal (--goal <key>); the named ones are reported beside it on every result, and decide nothing.`;
39456
+ }
39457
+ function pastTestLine(test) {
39458
+ const outcome = {
39459
+ supported: "the idea held",
39460
+ refuted_opposite: "the idea was WRONG \u2014 the change made it worse",
39461
+ refuted_no_effect: "no difference \u2014 the change did not matter",
39462
+ unanswered: "never answered"
39463
+ };
39464
+ const kept = test.survivor === "variant" ? "the new version was kept" : "the original was kept";
39465
+ const learning = test.learning ? ` Learning: ${test.learning}` : "";
39466
+ return `\u201C${test.hypothesis.change}\u201D (${test.experimentId}, ${test.ranForDays} days): ${outcome[test.hypothesisOutcome]}; ${kept}${test.againstVerdict ? " against the numbers" : ""}.${learning}`;
39467
+ }
39468
+ function pastTestsHint(plan) {
39469
+ if (plan.pastTests.length === 0) return null;
39470
+ const lines = plan.pastTests.slice(0, 5).map(pastTestLine);
39471
+ const more = plan.pastTests.length > 5 ? ` \u2026and ${plan.pastTests.length - 5} more \u2014 \`baker experiment history --landing <slug>\` lists them all.` : "";
39472
+ 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}`;
38847
39473
  }
38848
39474
  function buildPlanHints(plan) {
38849
39475
  const horizons = horizonsHint(plan);
39476
+ const past = pastTestsHint(plan);
38850
39477
  if (!plan.canRun) {
38851
39478
  return [
38852
39479
  plan.reason ?? "This test cannot run on this page.",
38853
39480
  ...horizons && plan.visitors > 0 ? [horizons] : [],
39481
+ ...past ? [past] : [],
38854
39482
  "A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
38855
39483
  ];
38856
39484
  }
38857
39485
  const hints2 = [
39486
+ ...past ? [past] : [],
39487
+ // First, because it changes how every number below should be read: a plan
39488
+ // sized on a stand-in is a guide, and the agent must say so to the client
39489
+ // rather than quote "about 70 days" as though it were measured.
39490
+ ...plan.warning ? [plan.warning] : [],
38858
39491
  `This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
38859
39492
  // Fires at the exact moment the hypothesis gets invented, whether or not
38860
39493
  // the agent read the family doc. `plan` says a question CAN be settled here
38861
39494
  // and says nothing about which question is worth asking — and a well-run
38862
- // test of a bad idea costs three weeks and returns `no_difference`.
39495
+ // test of a bad idea spends the page's traffic and returns `no_difference`.
38863
39496
  "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.",
38864
39497
  // The base always works, so a missing integration is never a reason to
38865
39498
  // stop. Naming what it would have shown is worth more to the client than
@@ -38870,12 +39503,60 @@ function buildPlanHints(plan) {
38870
39503
  if (conversions) hints2.push(conversions);
38871
39504
  if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
38872
39505
  hints2.push(
38873
- `At this page's traffic it would take about ${plan.estimatedDays} days. Consider testing a bigger change, which needs less traffic to detect.`
39506
+ `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.`
38874
39507
  );
38875
39508
  }
38876
39509
  if (horizons) hints2.push(horizons);
38877
39510
  return hints2;
38878
39511
  }
39512
+ function buildHistoryHints(tests, landing) {
39513
+ if (tests.length === 0) {
39514
+ return [
39515
+ 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."
39516
+ ];
39517
+ }
39518
+ const hints2 = [];
39519
+ const supported = tests.filter((test) => test.hypothesisOutcome === "supported");
39520
+ const refuted = tests.filter((test) => test.hypothesisOutcome === "refuted_opposite");
39521
+ const noEffect = tests.filter((test) => test.hypothesisOutcome === "refuted_no_effect");
39522
+ if (supported.length > 0) {
39523
+ hints2.push(
39524
+ `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.`
39525
+ );
39526
+ }
39527
+ if (refuted.length > 0) {
39528
+ hints2.push(
39529
+ `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.`
39530
+ );
39531
+ }
39532
+ if (noEffect.length > 0) {
39533
+ hints2.push(
39534
+ `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.`
39535
+ );
39536
+ }
39537
+ const unlearned = tests.filter((test) => test.learning === null && test.verdict !== "inconclusive");
39538
+ if (unlearned.length > 0) {
39539
+ hints2.push(
39540
+ `${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"\`.`
39541
+ );
39542
+ }
39543
+ return hints2;
39544
+ }
39545
+ function buildStartHints(staged) {
39546
+ const hints2 = [];
39547
+ if (staged.composition && staged.composition.forked.length === 0) {
39548
+ hints2.push(
39549
+ `\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.`
39550
+ );
39551
+ }
39552
+ if (staged.newForms.length > 0 && staged.variantGoal === void 0) {
39553
+ const forms = staged.newForms.map((flow) => `\u201C${flow}\u201D`).join(", ");
39554
+ hints2.push(
39555
+ `\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).`
39556
+ );
39557
+ }
39558
+ return hints2;
39559
+ }
38879
39560
 
38880
39561
  // ../api/src/experiments/composition.ts
38881
39562
  import { z as z35 } from "zod";
@@ -38888,7 +39569,8 @@ var experimentCompositionSchema = z35.object({
38888
39569
 
38889
39570
  // ../api/src/experiments/goal.ts
38890
39571
  import { z as z36 } from "zod";
38891
- var experimentGoalSchema = z36.string().trim().min(1).max(MAX_CONVERSION_NAME);
39572
+ var GOAL_KEY_HELP = "A goal is one event, by its key \u2014 `form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>` or `exit:<host>`. `baker analytics conversions --candidates` lists the ones these pages produce";
39573
+ var experimentGoalSchema = z36.string().trim().min(1).max(400).refine((key) => parseEventKey(key) !== null, { message: GOAL_KEY_HELP });
38892
39574
 
38893
39575
  // ../api/src/experiments/hypothesis.ts
38894
39576
  import { z as z37 } from "zod";
@@ -38943,9 +39625,13 @@ var experimentHypothesisOutcomeSchema = z37.enum([
38943
39625
  // ../api/src/experiments/wire.ts
38944
39626
  import { z as z38 } from "zod";
38945
39627
  var slugSchema = z38.string().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a page slug, like `oferta`");
39628
+ var variantSlugSchema = z38.string().min(1).max(130).refine((slug) => {
39629
+ const page = variantOfSlug(slug);
39630
+ return page !== null && slugSchema.safeParse(page).success;
39631
+ }, "Use the variant's slug, `<page>--<n>` \u2014 `baker landing variant <page>` chooses it");
38946
39632
  var experimentPlanRequestSchema = z38.object({
38947
39633
  landingSlug: slugSchema,
38948
- variantSlug: slugSchema,
39634
+ variantSlug: variantSlugSchema,
38949
39635
  /**
38950
39636
  * Which of the company's conversions the test is about.
38951
39637
  *
@@ -38955,6 +39641,15 @@ var experimentPlanRequestSchema = z38.object({
38955
39641
  * is measuring two things and deciding on their sum.
38956
39642
  */
38957
39643
  goal: experimentGoalSchema.optional(),
39644
+ /**
39645
+ * The event the NEW version converts on, when it is not the page's.
39646
+ *
39647
+ * The test asks one question — did more visitors do the thing we want — but
39648
+ * the thing can be a different event on each version: a variant that swaps
39649
+ * the page's Form for a shorter one converts on the new Form's ending. Absent,
39650
+ * the variant is read on `goal`, which is the common case.
39651
+ */
39652
+ variantGoal: experimentGoalSchema.optional(),
38958
39653
  /**
38959
39654
  * The smallest lift worth detecting, relative to the baseline. `0.2` is +20%.
38960
39655
  *
@@ -38963,10 +39658,40 @@ var experimentPlanRequestSchema = z38.object({
38963
39658
  * quarter of a million visitors per arm; `plan` will say so rather than
38964
39659
  * running it.
38965
39660
  */
38966
- minDetectableRelativeLift: z38.number().gt(0).max(10).optional()
39661
+ minDetectableRelativeLift: z38.number().gt(0).max(10).optional(),
39662
+ /**
39663
+ * The share of visitors expected to do the goal, as a fraction — `0.05` is
39664
+ * 5% — for a page whose history cannot say.
39665
+ *
39666
+ * A page about to get its first campaign has no rate to size on. Without
39667
+ * this the plan stands on the company-wide rate for the outcome, or on a
39668
+ * conventional 3%; with it, on what the person setting the test up knows.
39669
+ * Either way the brief says which, because the sizing is a guide and never a
39670
+ * finish line — the test runs until the numbers are clear.
39671
+ */
39672
+ baselineRate: z38.number().gt(0).lt(1).optional()
39673
+ });
39674
+ var experimentTrafficSchema = z38.enum(["none", "low", "enough"]);
39675
+ var experimentBaselineSourceSchema = z38.enum(["page", "company", "assumed"]);
39676
+ var experimentSizedOnSchema = z38.object({
39677
+ visitors: z38.number(),
39678
+ convertingVisitors: z38.number(),
39679
+ windowDays: z38.number()
38967
39680
  });
38968
39681
  var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
38969
39682
  hypothesis: experimentHypothesisSchema,
39683
+ /**
39684
+ * The two pages' own identities, read off their `_definition.md` by the CLI.
39685
+ *
39686
+ * The backend prefers the `landings` rows, but the variant has no row until
39687
+ * the Session that built it is published — which is after the test is
39688
+ * staged. Without this the variant's identity was never recorded, so a
39689
+ * renamed variant folder detached its running test and a fold archived the
39690
+ * loser under the wrong key. Optional because a workspace that cannot read
39691
+ * its own definition must still be able to start a test.
39692
+ */
39693
+ landingInternalId: z38.string().min(1).max(64).optional(),
39694
+ variantInternalId: z38.string().min(1).max(64).optional(),
38970
39695
  /**
38971
39696
  * What the two arms are made of, read off the import graph by the caller at
38972
39697
  * the moment the test starts.
@@ -38979,18 +39704,60 @@ var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
38979
39704
  composition: experimentCompositionSchema.optional()
38980
39705
  });
38981
39706
  var experimentKeepSchema = z38.enum(["original", "variant"]);
39707
+ var experimentEndReasonSchema = z38.enum(["numbers_clear", "not_settling", "page_changing", "test_flawed"]);
39708
+ var experimentLearningSchema = z38.string().trim().min(1).max(2e3);
38982
39709
  var experimentFinishRequestSchema = z38.object({
38983
39710
  experimentId: z38.string().min(1).max(64),
38984
- keep: experimentKeepSchema
39711
+ keep: experimentKeepSchema,
39712
+ /** Why it is being ended. The dashboard always asks; the agent should say. */
39713
+ reason: experimentEndReasonSchema.optional(),
39714
+ learning: experimentLearningSchema.optional()
38985
39715
  });
38986
39716
  var experimentPauseRequestSchema = z38.object({
38987
39717
  experimentId: z38.string().min(1).max(64)
38988
39718
  });
39719
+ var experimentUpdateRequestSchema = z38.object({
39720
+ experimentId: z38.string().min(1).max(64),
39721
+ because: experimentHypothesisSchema.shape.because.optional(),
39722
+ change: experimentHypothesisSchema.shape.change.optional(),
39723
+ evidence: experimentHypothesisSchema.shape.evidence,
39724
+ /**
39725
+ * Staged tests only. Spelled out rather than borrowed from the hypothesis
39726
+ * schema, whose `expect` defaults to `increase`: through `.optional()` that
39727
+ * default still fires, so every wording-only update arrived carrying a
39728
+ * direction and was refused for trying to change it.
39729
+ */
39730
+ expect: z38.enum(["increase", "decrease"]).optional(),
39731
+ /** Staged tests only. */
39732
+ goal: experimentGoalSchema.optional(),
39733
+ /** Staged tests only. The new version's own event, when it differs. */
39734
+ variantGoal: experimentGoalSchema.optional(),
39735
+ /** Staged tests only. */
39736
+ minDetectableRelativeLift: z38.number().gt(0).max(10).optional(),
39737
+ /** Finished tests only: what the test taught, written or corrected after the fact. */
39738
+ learning: experimentLearningSchema.optional()
39739
+ });
39740
+ var experimentUpdateResponseSchema = z38.object({
39741
+ /** True when the test had not been published, so its whole plan was re-sized. */
39742
+ staged: z38.boolean(),
39743
+ hypothesisStatement: z38.string()
39744
+ });
38989
39745
  var experimentFoldRequestSchema = z38.object({
38990
39746
  /** Which test to fold. Omitted ⇒ every fold this company owes. */
38991
39747
  experimentId: z38.string().max(64).optional(),
38992
39748
  /** The files are written. Baker drops the promotion once the build lands. */
38993
- applied: z38.boolean().optional()
39749
+ applied: z38.boolean().optional(),
39750
+ /**
39751
+ * Which of the owed folds were actually written, on an `applied` call.
39752
+ *
39753
+ * Folding is per page and one page failing must not hold back the others, so
39754
+ * a run that folds two of three has to be able to say which two. Without it
39755
+ * the report was "the company", every owed test was stamped as written, and
39756
+ * the one that failed had its promotion dropped when the build landed — so
39757
+ * its page served the version that LOST, with nothing owed and nothing
39758
+ * logged. Omitted ⇒ everything owed, which is the whole-company case.
39759
+ */
39760
+ experimentIds: z38.array(z38.string().max(64)).max(200).optional()
38994
39761
  });
38995
39762
  var experimentFoldTargetSchema = z38.object({
38996
39763
  experimentId: z38.string(),
@@ -39010,10 +39777,19 @@ var experimentFoldResponseSchema = z38.object({
39010
39777
  });
39011
39778
  var experimentStatusRequestSchema = z38.object({
39012
39779
  experimentId: z38.string().max(64).optional(),
39780
+ /** Only the tests on one page, by its slug. */
39781
+ landingSlug: z38.string().max(200).optional(),
39013
39782
  full: z38.boolean().optional()
39014
39783
  });
39784
+ var experimentHistoryRequestSchema = z38.object({
39785
+ landingSlug: z38.string().max(200).optional(),
39786
+ limit: z38.number().int().positive().max(100).optional()
39787
+ });
39015
39788
  var experimentPageConversionSchema = z38.object({
39016
- name: z38.string(),
39789
+ /** The event, by key — what `--goal` takes. */
39790
+ eventKey: z38.string(),
39791
+ /** The conversion name the company counts it under, or null when nobody has named it. */
39792
+ name: z38.string().nullable(),
39017
39793
  /** Distinct visitors to the page who went on to do this. */
39018
39794
  convertingVisitors: z38.number()
39019
39795
  });
@@ -39026,9 +39802,11 @@ var experimentPlanResponseSchema = z38.object({
39026
39802
  canRun: z38.boolean(),
39027
39803
  /** Present when `canRun` is false. Product language, ready to show. */
39028
39804
  reason: z38.string().optional(),
39029
- /** The conversion the test would be read on. Absent when none could be chosen. */
39805
+ /** The event the test would be read on. Absent when none could be chosen. */
39030
39806
  goal: experimentGoalSchema.optional(),
39031
- /** Every conversion this page produced over the sizing window, most common first. */
39807
+ /** The new version's own event, when one was given. */
39808
+ variantGoal: experimentGoalSchema.optional(),
39809
+ /** Every event this page's visitors went on to do over the sizing window, most common first. */
39032
39810
  conversions: z38.array(experimentPageConversionSchema),
39033
39811
  /** Distinct visitors the page had over the sizing window. */
39034
39812
  visitors: z38.number(),
@@ -39040,7 +39818,24 @@ var experimentPlanResponseSchema = z38.object({
39040
39818
  dailyVisitorsPerVariant: z38.number(),
39041
39819
  /** `null` when the page has no traffic to project from. */
39042
39820
  estimatedDays: z38.number().nullable(),
39043
- horizons: z38.array(experimentHorizonSchema)
39821
+ horizons: z38.array(experimentHorizonSchema),
39822
+ traffic: experimentTrafficSchema,
39823
+ baselineSource: experimentBaselineSourceSchema,
39824
+ sizedOn: experimentSizedOnSchema,
39825
+ /**
39826
+ * Why the sizing is a guide rather than a measurement, when it is one.
39827
+ * Product language, ready to show; absent on a page with real traffic that
39828
+ * has produced the goal.
39829
+ */
39830
+ warning: z38.string().optional(),
39831
+ /**
39832
+ * Every finished test on this page, newest first — see
39833
+ * `experimentHistoryEntrySchema`. On the plan because the plan is the moment
39834
+ * a hypothesis is chosen, and a page's own record is the first thing that
39835
+ * should inform it: a refuted idea proposed again, or a supported one never
39836
+ * built on, is what this exists to prevent.
39837
+ */
39838
+ pastTests: z38.array(z38.lazy(() => experimentHistoryEntrySchema)).default([])
39044
39839
  });
39045
39840
  var experimentVerdictSchema = z38.enum([
39046
39841
  "keep_running",
@@ -39073,7 +39868,18 @@ var experimentArmSchema = z38.object({
39073
39868
  * screens cannot show a client two different "this is your offer page".
39074
39869
  * `null` before the first capture, and on a page whose capture failed.
39075
39870
  */
39076
- previewUrl: z38.string().nullable().optional()
39871
+ previewUrl: z38.string().nullable().optional(),
39872
+ /**
39873
+ * This version as it was when the test started — the test's own capture,
39874
+ * taken off the live site once the split was being served.
39875
+ *
39876
+ * `previewUrl` above is the page as it is *today*, which is right while a
39877
+ * test runs and wrong the day after somebody edits the page, and absent
39878
+ * altogether for a variant that lost and was archived. This is the record;
39879
+ * a card prefers it and says which of the two it is showing.
39880
+ */
39881
+ snapshotUrl: z38.string().nullable().optional(),
39882
+ snapshotCapturedAt: z38.number().nullable().optional()
39077
39883
  });
39078
39884
  var experimentMetricSchema = z38.object({
39079
39885
  name: z38.string(),
@@ -39124,8 +39930,12 @@ var experimentStatusRowSchema = z38.object({
39124
39930
  * is not a hypothesis, "leads should go up by at least 20%" is.
39125
39931
  */
39126
39932
  minDetectableRelativeLift: z38.number(),
39127
- /** The conversion the verdict is read from. The company's own name for it. */
39933
+ /** The event the verdict is read from on the page, by key. */
39128
39934
  goal: experimentGoalSchema,
39935
+ /** The event the new version is read on, when it differs from `goal`. */
39936
+ variantGoal: experimentGoalSchema.nullable(),
39937
+ /** Both, in words a marketer reads — the Form's name, the moment. */
39938
+ goalLabel: z38.string(),
39129
39939
  status: experimentStatusValueSchema,
39130
39940
  startedAt: z38.number(),
39131
39941
  /** Set while paused. The split is off and everyone sees the original. */
@@ -39141,8 +39951,26 @@ var experimentStatusRowSchema = z38.object({
39141
39951
  survivor: z38.enum(["control", "variant"]).nullable(),
39142
39952
  /** Set only on `invalid`. Why the numbers cannot be read. */
39143
39953
  invalidReason: z38.enum(["sample_ratio_mismatch", "crossover"]).nullable(),
39954
+ /**
39955
+ * Where the page's own content stands after the test, once it has ended.
39956
+ *
39957
+ * `owed` — the new variant was kept and is served by a rewrite; the page's
39958
+ * source still says otherwise. `landing` — the files are written and the
39959
+ * next publish carries them. `done` — the page's own content is the version
39960
+ * that was kept, and nothing routes either arm any more. `none` — the
39961
+ * original was kept, so there was never anything to move.
39962
+ *
39963
+ * On the row because two things on the card read it: whether "open this
39964
+ * version" links still show the right file, and whether there is anything
39965
+ * left for a person to do.
39966
+ */
39967
+ fold: z38.enum(["none", "owed", "landing", "done"]).nullable(),
39144
39968
  /** What a person should be told, in one sentence, in product language. */
39145
39969
  summary: z38.string(),
39970
+ /** What the test taught, once somebody wrote it down. Null until then, and always null while it runs. */
39971
+ learning: z38.string().nullable(),
39972
+ /** Why it was ended, when whoever ended it said. Null while it runs. */
39973
+ endReason: experimentEndReasonSchema.nullable(),
39146
39974
  control: experimentArmSchema,
39147
39975
  variant: experimentArmSchema,
39148
39976
  /** Every other conversion the company counts, on the same two arms. */
@@ -39177,6 +40005,13 @@ var experimentStatusRowSchema = z38.object({
39177
40005
  evidence: z38.object({
39178
40006
  relativeLift: z38.number(),
39179
40007
  probabilityVariantBetter: z38.number(),
40008
+ /**
40009
+ * How sure the numbers are that the two versions differ at all, 0 to 1.
40010
+ * Anytime-valid — the same construction the verdict is read from, so it
40011
+ * holds however often it is read — and a winner is called exactly when
40012
+ * it crosses 95%. The figure a card leads with.
40013
+ */
40014
+ confidence: z38.number(),
39180
40015
  controlInterval: z38.tuple([z38.number(), z38.number()]),
39181
40016
  variantInterval: z38.tuple([z38.number(), z38.number()]),
39182
40017
  /** The anytime-valid interval on the relative lift — what the verdict is read from. */
@@ -39186,6 +40021,47 @@ var experimentStatusRowSchema = z38.object({
39186
40021
  var experimentStatusResponseSchema = z38.object({
39187
40022
  experiments: z38.array(experimentStatusRowSchema)
39188
40023
  });
40024
+ var experimentHistoryEntrySchema = z38.object({
40025
+ experimentId: z38.string(),
40026
+ landingSlug: z38.string(),
40027
+ variantSlug: z38.string(),
40028
+ startedAt: z38.number(),
40029
+ finishedAt: z38.number(),
40030
+ /** Days the split ran, whole. */
40031
+ ranForDays: z38.number(),
40032
+ hypothesis: experimentHypothesisSchema,
40033
+ hypothesisStatement: z38.string(),
40034
+ /** The sections the new version owned, as labels — what was actually different. */
40035
+ changed: z38.array(z38.string()),
40036
+ goal: experimentGoalSchema,
40037
+ variantGoal: experimentGoalSchema.nullable(),
40038
+ goalLabel: z38.string(),
40039
+ verdict: experimentVerdictSchema,
40040
+ /** Which version the numbers favoured, on `winner`. */
40041
+ winner: z38.enum(["control", "variant"]).nullable(),
40042
+ /** Which version kept the traffic. */
40043
+ survivor: z38.enum(["control", "variant"]),
40044
+ /** True when what was kept is not what the numbers favoured. */
40045
+ againstVerdict: z38.boolean(),
40046
+ hypothesisOutcome: experimentHypothesisOutcomeSchema,
40047
+ /** The numbers, as they were when the test ended. */
40048
+ result: z38.object({
40049
+ control: z38.object({ visitors: z38.number(), conversions: z38.number(), rate: z38.number() }),
40050
+ variant: z38.object({ visitors: z38.number(), conversions: z38.number(), rate: z38.number() }),
40051
+ relativeLift: z38.number(),
40052
+ liftInterval: z38.tuple([z38.number(), z38.number()]),
40053
+ confidence: z38.number(),
40054
+ secondary: z38.array(experimentMetricSchema)
40055
+ }),
40056
+ summary: z38.string(),
40057
+ /** Why it was ended, when whoever ended it said. */
40058
+ endReason: experimentEndReasonSchema.nullable(),
40059
+ /** What the test taught, or null when nobody has written it down yet. */
40060
+ learning: z38.string().nullable()
40061
+ });
40062
+ var experimentHistoryResponseSchema = z38.object({
40063
+ tests: z38.array(experimentHistoryEntrySchema)
40064
+ });
39189
40065
  var experimentFinishResponseSchema = z38.object({
39190
40066
  verdict: experimentVerdictSchema,
39191
40067
  survivor: z38.enum(["control", "variant"]),
@@ -39214,9 +40090,8 @@ function parseOne(raw) {
39214
40090
  }
39215
40091
  return href ? { source: source.data, note, href } : { source: source.data, note };
39216
40092
  }
39217
- function parseEvidence(raw) {
39218
- if (raw === void 0) return void 0;
39219
- const entries = (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry !== "");
40093
+ function parseEvidence(raw, rawArgs) {
40094
+ const entries = repeatedValues(rawArgs, "evidence", raw).map((entry) => entry.trim()).filter((entry) => entry !== "");
39220
40095
  if (entries.length === 0) return void 0;
39221
40096
  const parsed = [];
39222
40097
  for (const entry of entries) {
@@ -39227,10 +40102,65 @@ function parseEvidence(raw) {
39227
40102
  return parsed;
39228
40103
  }
39229
40104
 
40105
+ // src/commands/experiment/identity.ts
40106
+ import { readFile as readFile23 } from "fs/promises";
40107
+ import path30 from "path";
40108
+ async function readPageInternalId(root, slug) {
40109
+ try {
40110
+ const text2 = await readFile23(path30.resolve(root, "src", "pages", slug, "_definition.md"), "utf8");
40111
+ return /^internalId:\s*"?([^"\s]+)"?\s*$/m.exec(text2)?.[1];
40112
+ } catch {
40113
+ return void 0;
40114
+ }
40115
+ }
40116
+
40117
+ // src/commands/experiment/update.ts
40118
+ function buildUpdateRequest(args, rawArgs) {
40119
+ const expect = args.expect === void 0 ? void 0 : String(args.expect);
40120
+ if (expect !== void 0 && expect !== "increase" && expect !== "decrease") {
40121
+ return {
40122
+ error: "`--expect` is either `increase` or `decrease` \u2014 which way should the goal move if you are right?"
40123
+ };
40124
+ }
40125
+ const evidence = parseEvidence(args.evidence, rawArgs);
40126
+ if (evidence && "error" in evidence) return evidence;
40127
+ const lift = args.lift === void 0 ? void 0 : Number(args.lift);
40128
+ if (lift !== void 0 && !(lift > 0)) {
40129
+ return {
40130
+ error: "`--lift` is the smallest improvement worth detecting, relative \u2014 0.2 is +20%. It must be above zero."
40131
+ };
40132
+ }
40133
+ const request = {
40134
+ experimentId: String(args.id),
40135
+ ...typed("because", args.because),
40136
+ ...typed("change", args.change),
40137
+ ...typed("goal", args.goal),
40138
+ ...typed("variantGoal", args.variantGoal),
40139
+ ...typed("learning", args.learning),
40140
+ ...expect === void 0 ? {} : { expect },
40141
+ ...evidence ? { evidence } : {},
40142
+ ...lift === void 0 ? {} : { minDetectableRelativeLift: lift }
40143
+ };
40144
+ if (Object.keys(request).length === 1) {
40145
+ return {
40146
+ error: "Nothing to change. Pass --because, --change or --evidence; on a finished test, --learning; on a test that has not been published yet, also --expect, --goal, --variant-goal or --lift"
40147
+ };
40148
+ }
40149
+ return request;
40150
+ }
40151
+ function typed(key, value) {
40152
+ return value === void 0 ? {} : { [key]: String(value) };
40153
+ }
40154
+
39230
40155
  // src/commands/experiment/index.ts
39231
40156
  var GOAL_ARG = {
39232
40157
  type: "string",
39233
- description: "Which of this company's conversions the test is about, by the name on the Conversions screen \u2014 \u201CBooked a call\u201D, \u201CQuote requested\u201D. Fixed for the life of the test. Optional when the company counts exactly one outcome; required when it counts several, and `plan` lists them. Every other conversion is reported beside the goal on every result and decides nothing. If the outcome you need is not defined yet, define it first with `baker analytics conversions` \u2014 it applies to history, so no traffic is lost.",
40158
+ description: "The ONE event the test is read on, by its key: `form:<flow>:<node>:<trigger>` (a Form reaching a step), `submit:<flow>` (a Form finished), `page:<name>` (a page event or one the client's systems post), `exit:<host>` (a click out). `baker analytics conversions --candidates` lists the keys these pages produce, and `plan` lists the ones this page's visitors went on to do. Fixed for the life of the test. Optional when exactly one of them is a named conversion; otherwise say which. Every conversion the company has named is reported beside the goal and decides nothing.",
40159
+ required: false
40160
+ };
40161
+ var VARIANT_GOAL_ARG = {
40162
+ type: "string",
40163
+ description: "The event the NEW version is read on, when it is not the page's \u2014 a variant that swaps the Form for a shorter one converts on the new Form's ending (`submit:<new-flow>`, or its confirmation step's `form:` key). Same vocabulary as --goal. Leave it out when both versions convert on the same event, which is the common case.",
39234
40164
  required: false
39235
40165
  };
39236
40166
  var LIFT_ARG = {
@@ -39258,7 +40188,30 @@ var EVIDENCE_ARG = {
39258
40188
  description: "Where the observation came from, as `source:what it showed`, optionally `|link`. Repeatable. Sources: baker_analytics, clarity, ads, search_console, research, inspiration, client, hunch. Use `hunch` honestly when there is no data \u2014 a stated guess is worth more than a guess dressed as analytics.",
39259
40189
  required: false
39260
40190
  };
40191
+ var BASELINE_ARG = {
40192
+ type: "string",
40193
+ description: "The share of visitors you expect to do the goal, as a fraction \u2014 0.05 is 5% \u2014 for a page whose history cannot say: a new page, or one about to get its first campaign. Without it a page with no conversions yet is sized on the company-wide rate for the outcome, or on 3%. The sizing is a guide either way: the test runs until the numbers are clear.",
40194
+ required: false
40195
+ };
39261
40196
  var ID_ARG = { type: "string", description: "The test, by its id", required: true };
40197
+ var LEARNING_ARG = {
40198
+ type: "string",
40199
+ description: "What this test taught, in one or two sentences the client should believe about this page from now on \u2014 \u201Cthe price is not the objection, the install date is\u201D. The one part of the record the numbers cannot write, and what `plan` hands the next test on this page. Finished tests only.",
40200
+ required: false
40201
+ };
40202
+ var REASON_ARG = {
40203
+ type: "string",
40204
+ description: "Why the test is being ended, as distinct from what the numbers said: `numbers_clear` (the result was clear enough to act on), `not_settling` (too little traffic or too small a difference \u2014 waiting would not help), `page_changing` (the page has to change for another reason), `test_flawed` (the wrong goal, a broken variant). Always say \u2014 the dashboard asks a person the same question, and the next test on this page is planned differently after each answer.",
40205
+ required: false
40206
+ };
40207
+ var LANDING_FILTER_ARG = {
40208
+ type: "string",
40209
+ description: "Only the tests on one page, by its slug",
40210
+ required: false
40211
+ };
40212
+ function sizingHints(data) {
40213
+ return typeof data.warning === "string" ? [data.warning] : [];
40214
+ }
39262
40215
  function fail5(message) {
39263
40216
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
39264
40217
  process.exit(1);
@@ -39272,24 +40225,28 @@ function reportApiError(error) {
39272
40225
  }
39273
40226
  registerSchema({
39274
40227
  command: "experiment.plan",
39275
- description: "Whether a page has enough traffic to settle a question, before anything is built",
40228
+ description: "Whether a page can settle a question, and how long it might take, before anything is built",
39276
40229
  args: {
39277
40230
  landing: { type: "string", description: "The page under test, by slug", required: true },
39278
40231
  variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
39279
40232
  goal: GOAL_ARG,
39280
- lift: LIFT_ARG
40233
+ variantGoal: VARIANT_GOAL_ARG,
40234
+ lift: LIFT_ARG,
40235
+ baseline: BASELINE_ARG
39281
40236
  }
39282
40237
  });
39283
40238
  var planCommand = defineCommand119({
39284
40239
  meta: {
39285
40240
  name: "plan",
39286
- description: "Start here, BEFORE building the alternative page. Says whether this page gets enough traffic to settle the question, how long it would take, which conversions the page's visitors actually go on to do, and what a 14, 28, 56 or 90-day window could settle. Most landing pages cannot run most tests \u2014 finding that out now costs one call, and finding it out later costs three weeks and a decision made on noise."
40241
+ description: "Start here, BEFORE building the alternative page. Says which event the test would be read on, which events the page's visitors actually go on to do (by key, with the conversion name each is counted under), how long it might take at this page's traffic, and what a 14, 28, 56 or 90-day window could settle. The duration is a guide, never a finish line \u2014 a test ends when the numbers are clear, and a page with no traffic yet can still be tested (`warning` says what it was sized on). What IS refused is a test that could not be measured: a goal this page has demonstrably never produced."
39287
40242
  },
39288
40243
  args: {
39289
40244
  landing: { type: "string", description: "The page under test, by slug", required: true },
39290
40245
  variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
39291
40246
  goal: GOAL_ARG,
39292
- lift: LIFT_ARG
40247
+ variantGoal: VARIANT_GOAL_ARG,
40248
+ lift: LIFT_ARG,
40249
+ baseline: BASELINE_ARG
39293
40250
  },
39294
40251
  run: async ({ args }) => {
39295
40252
  try {
@@ -39297,7 +40254,9 @@ var planCommand = defineCommand119({
39297
40254
  landingSlug: String(args.landing),
39298
40255
  variantSlug: String(args.variant),
39299
40256
  ...args.goal ? { goal: String(args.goal) } : {},
39300
- ...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {}
40257
+ ...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
40258
+ ...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
40259
+ ...args.baseline ? { baselineRate: Number(args.baseline) } : {}
39301
40260
  });
39302
40261
  writeJsonEnvelope({ ok: true, data: response.data, hints: buildPlanHints(response.data) });
39303
40262
  } catch (error) {
@@ -39316,13 +40275,15 @@ registerSchema({
39316
40275
  expect: EXPECT_ARG,
39317
40276
  evidence: EVIDENCE_ARG,
39318
40277
  goal: GOAL_ARG,
39319
- lift: LIFT_ARG
40278
+ variantGoal: VARIANT_GOAL_ARG,
40279
+ lift: LIFT_ARG,
40280
+ baseline: BASELINE_ARG
39320
40281
  }
39321
40282
  });
39322
40283
  var startCommand = defineCommand119({
39323
40284
  meta: {
39324
40285
  name: "start",
39325
- description: "Stage a test between the page and its new variant. Visitors are split 50/50 at the edge, so there is no flicker and nothing for an ad blocker to suppress. Nothing goes live until the session is published \u2014 the person publishing is the review of the new variant. Re-runs `plan` first and refuses on the same grounds."
40286
+ description: "Stage a test between the page and its new variant. Visitors are split between the two versions at the edge, so there is no flicker and nothing for an ad blocker to suppress. Nothing goes live until the session is published \u2014 the person publishing is the review of the new variant. Re-runs `plan` first and refuses on the same grounds."
39326
40287
  },
39327
40288
  args: {
39328
40289
  landing: { type: "string", description: "The page under test, by slug", required: true },
@@ -39332,20 +40293,33 @@ var startCommand = defineCommand119({
39332
40293
  expect: EXPECT_ARG,
39333
40294
  evidence: EVIDENCE_ARG,
39334
40295
  goal: GOAL_ARG,
39335
- lift: LIFT_ARG
40296
+ variantGoal: VARIANT_GOAL_ARG,
40297
+ lift: LIFT_ARG,
40298
+ baseline: BASELINE_ARG
39336
40299
  },
39337
- run: async ({ args }) => {
40300
+ run: async ({ args, rawArgs }) => {
39338
40301
  const expect = args.expect === void 0 ? "increase" : String(args.expect);
39339
40302
  if (expect !== "increase" && expect !== "decrease") {
39340
40303
  fail5("`--expect` is either `increase` or `decrease` \u2014 which way should the goal move if you are right?");
39341
40304
  }
39342
- const evidence = parseEvidence(args.evidence);
40305
+ const evidence = parseEvidence(args.evidence, rawArgs);
39343
40306
  if (evidence && "error" in evidence) fail5(evidence.error);
39344
40307
  const composition = await readComposition(process.cwd(), String(args.landing), String(args.variant));
40308
+ const [controlForms, variantForms] = await Promise.all([
40309
+ readForms(process.cwd(), String(args.landing)),
40310
+ readForms(process.cwd(), String(args.variant))
40311
+ ]);
40312
+ const newForms = formsOnlyIn(variantForms, controlForms);
40313
+ const [landingInternalId, variantInternalId] = await Promise.all([
40314
+ readPageInternalId(process.cwd(), String(args.landing)),
40315
+ readPageInternalId(process.cwd(), String(args.variant))
40316
+ ]);
39345
40317
  try {
39346
40318
  const response = await apiPost("/api/experiments/start", {
39347
40319
  landingSlug: String(args.landing),
39348
40320
  variantSlug: String(args.variant),
40321
+ ...landingInternalId ? { landingInternalId } : {},
40322
+ ...variantInternalId ? { variantInternalId } : {},
39349
40323
  hypothesis: {
39350
40324
  because: String(args.because),
39351
40325
  change: String(args.change),
@@ -39354,18 +40328,23 @@ var startCommand = defineCommand119({
39354
40328
  },
39355
40329
  ...composition ? { composition } : {},
39356
40330
  ...args.goal ? { goal: String(args.goal) } : {},
39357
- ...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {}
40331
+ ...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
40332
+ ...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
40333
+ ...args.baseline ? { baselineRate: Number(args.baseline) } : {}
39358
40334
  });
39359
40335
  writeJsonEnvelope({
39360
40336
  ok: true,
39361
40337
  data: response.data,
39362
40338
  hints: [
39363
- // The one composition worth interrupting for: `baker landing variant`
39364
- // with no `--fork` succeeds and produces a page that renders exactly
39365
- // the control, so the test would run for weeks against itself.
39366
- ...composition && composition.forked.length === 0 ? [
39367
- `\u201C${String(args.variant)}\u201D does not have its own copy of any section \u2014 it renders exactly the same page as \u201C${String(args.landing)}\u201D, so this test cannot find anything. Fork the section you want to test (\`baker landing variant ${String(args.landing)} --fork Hero.astro\`) and edit only that file.`
39368
- ] : [],
40339
+ ...sizingHints(response.data),
40340
+ ...buildStartHints({
40341
+ landing: String(args.landing),
40342
+ variant: String(args.variant),
40343
+ goal: String(response.data.goal),
40344
+ variantGoal: args.variantGoal === void 0 ? void 0 : String(args.variantGoal),
40345
+ composition,
40346
+ newForms
40347
+ }),
39369
40348
  "Staged. The split starts when this session is published \u2014 publishing is the review of the new variant.",
39370
40349
  "Check it as often as you like \u2014 `baker experiment status` is safe to read at any moment and holds its error rate however often you ask. What is never safe is reading the numbers instead of the verdict: `keep_running` means this test cannot yet tell the two pages apart, whatever the rates happen to say today."
39371
40350
  ]
@@ -39380,6 +40359,7 @@ registerSchema({
39380
40359
  description: "The verdict on every A/B test, running, paused and finished",
39381
40360
  args: {
39382
40361
  id: { type: "string", description: "One test, by its id", required: false },
40362
+ landing: LANDING_FILTER_ARG,
39383
40363
  full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
39384
40364
  }
39385
40365
  });
@@ -39390,12 +40370,14 @@ var statusCommand6 = defineCommand119({
39390
40370
  },
39391
40371
  args: {
39392
40372
  id: { type: "string", description: "One test, by its id", required: false },
40373
+ landing: LANDING_FILTER_ARG,
39393
40374
  full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
39394
40375
  },
39395
40376
  run: async ({ args }) => {
39396
40377
  try {
39397
40378
  const response = await apiPost("/api/experiments/status", {
39398
40379
  ...args.id ? { experimentId: String(args.id) } : {},
40380
+ ...args.landing ? { landingSlug: String(args.landing) } : {},
39399
40381
  ...args.full === true ? { full: true } : {}
39400
40382
  });
39401
40383
  writeJsonEnvelope({
@@ -39408,6 +40390,41 @@ var statusCommand6 = defineCommand119({
39408
40390
  }
39409
40391
  }
39410
40392
  });
40393
+ registerSchema({
40394
+ command: "experiment.history",
40395
+ description: "Every finished A/B test with what it found and what it taught \u2014 the record to read before proposing the next one",
40396
+ args: {
40397
+ landing: LANDING_FILTER_ARG,
40398
+ limit: { type: "string", description: "At most this many, newest first (default all)", required: false }
40399
+ }
40400
+ });
40401
+ var historyCommand = defineCommand119({
40402
+ meta: {
40403
+ name: "history",
40404
+ description: "What was tested before and what it taught. Finished tests only, newest first: the claim, what was different, what the numbers said, what was kept and whether that went against them, and the `learning` somebody wrote down. Read it \u2014 `--landing <slug>` for one page \u2014 BEFORE proposing a test: a refuted idea proposed again spends the page's traffic to return the same answer, and a supported one is what the next test should build on. `plan` hands the same entries back as `pastTests` for the page it sizes."
40405
+ },
40406
+ args: {
40407
+ landing: LANDING_FILTER_ARG,
40408
+ limit: { type: "string", description: "At most this many, newest first", required: false }
40409
+ },
40410
+ run: async ({ args }) => {
40411
+ const limit = args.limit === void 0 ? void 0 : Number(args.limit);
40412
+ if (limit !== void 0 && !(Number.isInteger(limit) && limit > 0)) fail5("`--limit` is a whole number above zero");
40413
+ try {
40414
+ const response = await apiPost("/api/experiments/history", {
40415
+ ...args.landing ? { landingSlug: String(args.landing) } : {},
40416
+ ...limit === void 0 ? {} : { limit }
40417
+ });
40418
+ writeJsonEnvelope({
40419
+ ok: true,
40420
+ data: response.data,
40421
+ hints: buildHistoryHints(response.data.tests, args.landing === void 0 ? void 0 : String(args.landing))
40422
+ });
40423
+ } catch (error) {
40424
+ reportApiError(error);
40425
+ }
40426
+ }
40427
+ });
39411
40428
  registerSchema({
39412
40429
  command: "experiment.finish",
39413
40430
  description: "End a test now and send all its traffic to the version you name",
@@ -39417,9 +40434,34 @@ registerSchema({
39417
40434
  type: "string",
39418
40435
  description: "Which version stays live: `original` or `variant`. Required \u2014 the verdict is a recommendation and this is the decision. A test that has not concluded can be ended too; it is recorded as inconclusive, never as a finding. Going against a verdict is allowed and is recorded as such.",
39419
40436
  required: true
39420
- }
40437
+ },
40438
+ reason: REASON_ARG,
40439
+ learning: LEARNING_ARG
39421
40440
  }
39422
40441
  });
40442
+ var END_REASONS = ["numbers_clear", "not_settling", "page_changing", "test_flawed"];
40443
+ function finishHints(data, given) {
40444
+ const { reason } = given;
40445
+ const args = { learning: given.learning };
40446
+ return [
40447
+ `Done. Everyone now sees the ${data.survivor === "variant" ? "new variant" : "original"}.`,
40448
+ ...data.againstVerdict ? [
40449
+ `You kept the ${data.survivor === "variant" ? "new variant" : "original"} although the numbers favoured the other version (${data.verdict}). That is recorded on the test \u2014 say why in the write-up.`
40450
+ ] : [],
40451
+ ...data.verdict === "keep_running" ? [
40452
+ "This test had not concluded, so it is recorded as inconclusive \u2014 nobody found out. Never report it as no difference."
40453
+ ] : [],
40454
+ ...data.survivor === "variant" ? [
40455
+ "Now run `baker experiment fold` \u2014 it moves the winning version into the page itself and archives the other one. Until that lands the page is served by a rewrite, and it cannot be tested again."
40456
+ ] : [],
40457
+ ...args.learning || data.verdict === "keep_running" ? [] : [
40458
+ 'Write down what this taught while it is fresh: `baker experiment update --id <id> --learning "\u2026"` \u2014 one or two sentences the client should believe about this page now. It is what the next test on this page starts from.'
40459
+ ],
40460
+ ...reason ? [] : [
40461
+ "No `--reason` was given, so the record says what the numbers had reached but not why anyone stopped looking. Next time pass one: numbers_clear, not_settling, page_changing or test_flawed."
40462
+ ]
40463
+ ];
40464
+ }
39423
40465
  var finishCommand = defineCommand119({
39424
40466
  meta: {
39425
40467
  name: "finish",
@@ -39431,17 +40473,60 @@ var finishCommand = defineCommand119({
39431
40473
  type: "string",
39432
40474
  description: "Which version stays live: `original` or `variant`.",
39433
40475
  required: true
39434
- }
40476
+ },
40477
+ reason: REASON_ARG,
40478
+ learning: LEARNING_ARG
39435
40479
  },
39436
40480
  run: async ({ args }) => {
39437
40481
  const keep = String(args.keep);
39438
40482
  if (keep !== "original" && keep !== "variant") {
39439
40483
  fail5("`--keep` is either `original` or `variant` \u2014 which version should everyone see from now on?");
39440
40484
  }
40485
+ const reason = args.reason === void 0 ? void 0 : String(args.reason);
40486
+ if (reason !== void 0 && !END_REASONS.some((known) => known === reason)) {
40487
+ fail5(`\`--reason\` is one of ${END_REASONS.join(", ")} \u2014 why is this test being ended?`);
40488
+ }
39441
40489
  try {
39442
40490
  const response = await apiPost(
39443
40491
  "/api/experiments/finish",
39444
- { experimentId: String(args.id), keep }
40492
+ {
40493
+ experimentId: String(args.id),
40494
+ keep,
40495
+ ...reason ? { reason } : {},
40496
+ ...args.learning ? { learning: String(args.learning) } : {}
40497
+ }
40498
+ );
40499
+ if ("unstaged" in response.data) {
40500
+ writeJsonEnvelope({
40501
+ ok: true,
40502
+ data: response.data,
40503
+ hints: ["That test had not been published yet, so it was taken back off this session. Nothing was split."]
40504
+ });
40505
+ return;
40506
+ }
40507
+ const data = response.data;
40508
+ writeJsonEnvelope({ ok: true, data, hints: finishHints(data, { reason, learning: args.learning }) });
40509
+ } catch (error) {
40510
+ reportApiError(error);
40511
+ }
40512
+ }
40513
+ });
40514
+ registerSchema({
40515
+ command: "experiment.cancel",
40516
+ description: "Call a test off \u2014 take a staged one back off the session, or end a live one keeping the original",
40517
+ args: { id: ID_ARG }
40518
+ });
40519
+ var cancelCommand = defineCommand119({
40520
+ meta: {
40521
+ name: "cancel",
40522
+ description: "Call a test off. A test staged in this session and not yet published is simply taken back off it \u2014 nothing was split. A test that is live is ended now with everyone sent back to the original: what the numbers had said is recorded as it stood (usually `inconclusive`), and the variant page stays in the workspace, unserved. `finish --keep` is the same act with the choice of version; this is the shortcut for \u201Cstop, keep the page as it is\u201D."
40523
+ },
40524
+ args: { id: ID_ARG },
40525
+ run: async ({ args }) => {
40526
+ try {
40527
+ const response = await apiPost(
40528
+ "/api/experiments/finish",
40529
+ { experimentId: String(args.id), keep: "original" }
39445
40530
  );
39446
40531
  if ("unstaged" in response.data) {
39447
40532
  writeJsonEnvelope({
@@ -39456,16 +40541,68 @@ var finishCommand = defineCommand119({
39456
40541
  ok: true,
39457
40542
  data,
39458
40543
  hints: [
39459
- `Done. Everyone now sees the ${data.survivor === "variant" ? "new variant" : "original"}.`,
39460
- ...data.againstVerdict ? [
39461
- `You kept the ${data.survivor === "variant" ? "new variant" : "original"} although the numbers favoured the other version (${data.verdict}). That is recorded on the test \u2014 say why in the write-up.`
39462
- ] : [],
40544
+ "Ended. Everyone sees the original page now, and the split stopped within seconds.",
39463
40545
  ...data.verdict === "keep_running" ? [
39464
- "This test had not concluded, so it is recorded as inconclusive \u2014 nobody found out. Never report it as no difference."
40546
+ "It had not concluded, so it is recorded as inconclusive \u2014 nobody found out. Never report it as no difference."
40547
+ ] : data.againstVerdict ? [
40548
+ `The numbers favoured the new variant (${data.verdict}), so this is recorded as a decision against them. Say why in the write-up.`
39465
40549
  ] : [],
39466
- ...data.survivor === "variant" ? [
39467
- "Now run `baker experiment fold` \u2014 it moves the winning version into the page itself and archives the other one. Until that lands the page is served by a rewrite, and it cannot be tested again."
39468
- ] : []
40550
+ "The variant page is still in the workspace and served to nobody. Run `baker experiment fold` to archive it, or leave it for a later test."
40551
+ ]
40552
+ });
40553
+ } catch (error) {
40554
+ reportApiError(error);
40555
+ }
40556
+ }
40557
+ });
40558
+ registerSchema({
40559
+ command: "experiment.update",
40560
+ description: "Change what a test says about itself \u2014 its wording always, its measurement only while it is still staged",
40561
+ args: {
40562
+ id: ID_ARG,
40563
+ because: { ...BECAUSE_ARG, required: false },
40564
+ change: { ...CHANGE_ARG, required: false },
40565
+ evidence: EVIDENCE_ARG,
40566
+ expect: { ...EXPECT_ARG, description: "Staged tests only. `increase` or `decrease`." },
40567
+ goal: { ...GOAL_ARG, description: "Staged tests only. The event the test is read on, by key." },
40568
+ variantGoal: {
40569
+ ...VARIANT_GOAL_ARG,
40570
+ description: "Staged tests only. The new version's own event, when it differs."
40571
+ },
40572
+ lift: { ...LIFT_ARG, description: "Staged tests only. The smallest lift worth detecting." },
40573
+ learning: LEARNING_ARG
40574
+ }
40575
+ });
40576
+ var updateCommand3 = defineCommand119({
40577
+ meta: {
40578
+ name: "update",
40579
+ description: "Change what a test says about itself. The observation (`--because`), the change in the client's words (`--change`) and the evidence cited (`--evidence`) can change at any time \u2014 they describe the test, and a write-up corrected after the fact is still the write-up of the same test. On a FINISHED test, `--learning` writes down what it taught. What the test MEASURES \u2014 `--goal`, `--lift`, `--expect` \u2014 can only change while it is still staged in this session: once visitors have been split, moving the goal or the finish line is how a result gets argued into existence. To measure something else on a live test, `finish` it and start a new one."
40580
+ },
40581
+ args: {
40582
+ id: ID_ARG,
40583
+ because: { ...BECAUSE_ARG, required: false },
40584
+ change: { ...CHANGE_ARG, required: false },
40585
+ evidence: EVIDENCE_ARG,
40586
+ expect: { ...EXPECT_ARG, description: "Staged tests only. `increase` or `decrease`." },
40587
+ goal: { ...GOAL_ARG, description: "Staged tests only. The event the test is read on, by key." },
40588
+ variantGoal: {
40589
+ ...VARIANT_GOAL_ARG,
40590
+ description: "Staged tests only. The new version's own event, when it differs."
40591
+ },
40592
+ lift: { ...LIFT_ARG, description: "Staged tests only. The smallest lift worth detecting." },
40593
+ learning: LEARNING_ARG
40594
+ },
40595
+ run: async ({ args, rawArgs }) => {
40596
+ const request = buildUpdateRequest(args, rawArgs);
40597
+ if ("error" in request) fail5(request.error);
40598
+ try {
40599
+ const response = await apiPost("/api/experiments/update", request);
40600
+ writeJsonEnvelope({
40601
+ ok: true,
40602
+ data: response.data,
40603
+ hints: [
40604
+ response.data.staged ? "Updated and re-sized. It still starts when this session is published." : request.learning !== void 0 ? "Updated. What this test taught is on its record now, and `plan` hands it to the next test on this page." : "Updated. The test keeps running exactly as it was \u2014 only its wording changed.",
40605
+ `It now reads: ${response.data.hypothesisStatement}`
39469
40606
  ]
39470
40607
  });
39471
40608
  } catch (error) {
@@ -39516,7 +40653,7 @@ var resumeCommand = defineCommand119({
39516
40653
  writeJsonEnvelope({
39517
40654
  ok: true,
39518
40655
  data: { resumed: true },
39519
- hints: ["Resumed. Half the visitors see the new variant again."]
40656
+ hints: ["Resumed. The split is back on, with the same sides."]
39520
40657
  });
39521
40658
  } catch (error) {
39522
40659
  reportApiError(error);
@@ -39562,6 +40699,7 @@ var foldCommand = defineCommand119({
39562
40699
  if (folded.length > 0) {
39563
40700
  await apiPost("/api/experiments/fold", {
39564
40701
  applied: true,
40702
+ experimentIds: folded.map((result) => result.experimentId),
39565
40703
  ...args.id ? { experimentId: String(args.id) } : {}
39566
40704
  });
39567
40705
  }
@@ -39589,10 +40727,13 @@ var experimentCommand = defineCommand119({
39589
40727
  subCommands: {
39590
40728
  plan: planCommand,
39591
40729
  start: startCommand,
40730
+ update: updateCommand3,
39592
40731
  status: statusCommand6,
40732
+ history: historyCommand,
39593
40733
  pause: pauseCommand,
39594
40734
  resume: resumeCommand,
39595
40735
  finish: finishCommand,
40736
+ cancel: cancelCommand,
39596
40737
  fold: foldCommand
39597
40738
  }
39598
40739
  });
@@ -39603,176 +40744,6 @@ import { defineCommand as defineCommand124 } from "citty";
39603
40744
  // src/commands/flows/add.ts
39604
40745
  import { randomUUID } from "crypto";
39605
40746
  import { defineCommand as defineCommand120 } from "citty";
39606
-
39607
- // src/commands/flows/shared.ts
39608
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
39609
- import { join as join3 } from "path";
39610
- var FLOWS_DIR = "src/lib/flow-engine/flows";
39611
- function flowsDir() {
39612
- let dir = process.cwd();
39613
- for (let i = 0; i < 6; i++) {
39614
- const candidate = join3(dir, FLOWS_DIR);
39615
- if (existsSync5(candidate)) {
39616
- return candidate;
39617
- }
39618
- const parent = join3(dir, "..");
39619
- if (parent === dir) {
39620
- break;
39621
- }
39622
- dir = parent;
39623
- }
39624
- return join3(process.cwd(), FLOWS_DIR);
39625
- }
39626
- function failLocal(message) {
39627
- writeJson({ ok: false, error: { code: "NOT_FOUND", message } });
39628
- process.exit(1);
39629
- }
39630
- function listFlowSlugs() {
39631
- const dir = flowsDir();
39632
- if (!existsSync5(dir)) {
39633
- return [];
39634
- }
39635
- return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
39636
- }
39637
- function flowDataPath(slug) {
39638
- return join3(flowsDir(), slug, "_data.json");
39639
- }
39640
- function writeFlowTree(slug, tree) {
39641
- writeFileSync3(flowDataPath(slug), `${JSON.stringify(tree, null, 2)}
39642
- `, "utf-8");
39643
- }
39644
- function walkNodes(node, acc = []) {
39645
- if (!node || typeof node !== "object") return acc;
39646
- acc.push(node);
39647
- if (Array.isArray(node.children)) {
39648
- for (const child of node.children) walkNodes(child, acc);
39649
- }
39650
- return acc;
39651
- }
39652
- function collectSideEffects(tree) {
39653
- return walkNodes(tree).flatMap(
39654
- (node) => Array.isArray(node.sideEffects) ? node.sideEffects.filter((sideEffect) => Boolean(sideEffect) && typeof sideEffect === "object").map((sideEffect) => ({ node, sideEffect })) : []
39655
- );
39656
- }
39657
- function readFlowTree(slug) {
39658
- const path42 = join3(flowsDir(), slug, "_data.json");
39659
- if (!existsSync5(path42)) {
39660
- failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
39661
- }
39662
- try {
39663
- return JSON.parse(readFileSync11(path42, "utf-8"));
39664
- } catch (error) {
39665
- failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
39666
- }
39667
- }
39668
- function isConfigured(value) {
39669
- return typeof value === "string" && value.length > 0;
39670
- }
39671
- function nodeMeta(record) {
39672
- const nodeId = typeof record.id === "string" ? record.id : "";
39673
- const nodeName = typeof record.name === "string" ? record.name : void 0;
39674
- return { nodeId, ...nodeName ? { nodeName } : {} };
39675
- }
39676
- function widgetNodeStatus(record, meta) {
39677
- const nodeData = record.nodeData;
39678
- const nodeType = nodeData?.type;
39679
- if (typeof nodeType !== "string" || !FLOW_RESOURCE_NODE_TYPES.includes(nodeType)) {
39680
- return null;
39681
- }
39682
- const external = nodeData?.form?.external;
39683
- const selected = external != null && typeof external === "object";
39684
- const name = selected ? external.name : void 0;
39685
- return {
39686
- ...meta,
39687
- target: "node",
39688
- kind: nodeType,
39689
- status: selected ? `${nodeType} \u2014 selected${typeof name === "string" ? ` "${name}"` : ""}` : `${nodeType} \u2014 not selected`,
39690
- needsInput: !selected
39691
- };
39692
- }
39693
- function sideEffectNeedsInput(raw) {
39694
- const effect = raw;
39695
- const type = effect?.type;
39696
- if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
39697
- return false;
39698
- }
39699
- const configured = isConfigured(effect?.encryptedConfig);
39700
- if (!isFlowOauthSideEffect(type)) return !configured;
39701
- return !isConfigured(effect?.oauthProviderId) || !configured;
39702
- }
39703
- function sideEffectStatus(raw, meta) {
39704
- const effect = raw;
39705
- const type = effect?.type;
39706
- if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
39707
- return null;
39708
- }
39709
- const configured = isConfigured(effect?.encryptedConfig);
39710
- const oauth = isFlowOauthSideEffect(type);
39711
- const connected = isConfigured(effect?.oauthProviderId);
39712
- const needs = FLOW_SECRET_FIELDS[type];
39713
- const status = oauth ? `${type} \u2014 connection ${connected ? "[connected]" : "[needs connection]"}, config ${configured ? "[set]" : "[missing]"}` : configured ? `${type} \u2014 [configured]` : `${type} \u2014 [missing]${needs.length > 0 ? ` (needs: ${needs.join(", ")})` : ""}`;
39714
- return {
39715
- ...meta,
39716
- target: "sideEffect",
39717
- ...typeof effect?.id === "string" ? { sideEffectId: effect.id } : {},
39718
- kind: type,
39719
- status,
39720
- needsInput: sideEffectNeedsInput(effect)
39721
- };
39722
- }
39723
- function requestFlowInputCall(slug, status) {
39724
- const args = status.target === "node" ? `{ target: "node", flowSlug: "${slug}", nodeId: "${status.nodeId}", nodeType: "${status.kind}" }` : `{ target: "sideEffect", flowSlug: "${slug}", nodeId: "${status.nodeId}", sideEffectId: "${status.sideEffectId}", sideEffectType: "${status.kind}" }`;
39725
- return `request_flow_input with ${args}`;
39726
- }
39727
- function collectConfigStatus(node, acc) {
39728
- if (!node || typeof node !== "object") {
39729
- return;
39730
- }
39731
- const record = node;
39732
- const meta = nodeMeta(record);
39733
- const widget = widgetNodeStatus(record, meta);
39734
- if (widget) {
39735
- acc.push(widget);
39736
- }
39737
- if (Array.isArray(record.sideEffects)) {
39738
- for (const raw of record.sideEffects) {
39739
- const status = sideEffectStatus(raw, meta);
39740
- if (status) {
39741
- acc.push(status);
39742
- }
39743
- }
39744
- }
39745
- if (Array.isArray(record.children)) {
39746
- for (const child of record.children) {
39747
- collectConfigStatus(child, acc);
39748
- }
39749
- }
39750
- }
39751
- function redactTree(node) {
39752
- if (Array.isArray(node)) {
39753
- return node.map(redactTree);
39754
- }
39755
- if (!node || typeof node !== "object") {
39756
- return node;
39757
- }
39758
- const out = {};
39759
- for (const [key, value] of Object.entries(node)) {
39760
- if (key === "encryptedConfig") {
39761
- out[key] = isConfigured(value) ? "[configured]" : value;
39762
- } else {
39763
- out[key] = redactTree(value);
39764
- }
39765
- }
39766
- return out;
39767
- }
39768
- function stepNameKey(name) {
39769
- return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
39770
- }
39771
- function isUsableStepNameKey(key) {
39772
- return /^[\p{ID_Start}$_][\p{ID_Continue}$‌‍]*$/u.test(key);
39773
- }
39774
-
39775
- // src/commands/flows/add.ts
39776
40747
  function unknownType(kind, type) {
39777
40748
  writeJson({
39778
40749
  ok: false,
@@ -39863,6 +40834,10 @@ var addNodeCommand = defineCommand120({
39863
40834
  data: { slug, nodeId: node.id, type, parentId: parent.id ?? null, path: flowDataPath(slug) },
39864
40835
  hints: [
39865
40836
  `Now fill in this step's \`ai\` field and its copy by editing ${flowDataPath(slug)}.`,
40837
+ // Said on every step added, because the moment a Form's shape changes
40838
+ // is the moment its outcome can move — and a Form nobody named an
40839
+ // outcome for reports zero conversions rather than an error.
40840
+ `Say what this Form counts before you finish: \`baker analytics conversions --flow ${slug}\` lists every ending already spelled as an event key, including this step's. Nothing in _data.json decides it.`,
39866
40841
  ...entry.owners?.["nodeData.form.external"] === "request_flow_input" ? [
39867
40842
  `A ${type} node's resource is picked by the user: call request_flow_input with { target: "node", flowSlug: "${slug}", nodeId: "${node.id}", nodeType: "${type}" }.`
39868
40843
  ] : [],
@@ -39976,6 +40951,13 @@ var addSideEffectCommand = defineCommand120({
39976
40951
  data: { slug, nodeId: node.id, sideEffectId: id, type, triggerId, path: flowDataPath(slug) },
39977
40952
  hints: [
39978
40953
  `Fires on \`${triggerId}\` \u2014 the moment "${String(node.name ?? node.id)}" raises. A side effect only runs when its triggerId is the one its own step raises, so this is set from the step, never from the side-effect type.`,
40954
+ // A pixel side effect is the thing most often mistaken for "setting the
40955
+ // conversion". It tells the ad platform; it tells Baker nothing, and a
40956
+ // Form can fire every pixel it has while every Baker report says it
40957
+ // converted nobody.
40958
+ ...needsUser.some(([, owner]) => owner === "tags") ? [
40959
+ `This reports the conversion to ${type}. It does NOT make Baker count anything: what Baker counts is a named event, set with \`baker analytics conversions --flow ${slug}\`. Do both \u2014 they are two different systems asked the same question.`
40960
+ ] : [],
39979
40961
  ...needsUser.some(([, owner]) => owner === "request_flow_input") ? [
39980
40962
  `Configure it: request_flow_input with { target: "sideEffect", flowSlug: "${slug}", nodeId: "${node.id}", sideEffectId: "${id}", sideEffectType: "${type}" }. Read the destination straight off that call's result.`
39981
40963
  ] : [],
@@ -39989,11 +40971,11 @@ var addSideEffectCommand = defineCommand120({
39989
40971
  });
39990
40972
 
39991
40973
  // src/commands/flows/map.ts
39992
- import { readFileSync as readFileSync13 } from "fs";
40974
+ import { readFileSync as readFileSync14 } from "fs";
39993
40975
  import { defineCommand as defineCommand121 } from "citty";
39994
40976
 
39995
40977
  // src/commands/flows/value-expression.ts
39996
- import { existsSync as existsSync6, readFileSync as readFileSync12 } from "fs";
40978
+ import { existsSync as existsSync6, readFileSync as readFileSync13 } from "fs";
39997
40979
  import { join as join4 } from "path";
39998
40980
  var PREFIXES = ["field", "tracking", "global", "code"];
39999
40981
  function splitParts(raw) {
@@ -40052,10 +41034,10 @@ function parseValueExpression(raw) {
40052
41034
  return parts.map(parsePart);
40053
41035
  }
40054
41036
  function trackingFieldIds() {
40055
- const path42 = join4(flowsDir(), "..", "tracking.ts");
40056
- if (!existsSync6(path42)) return null;
41037
+ const path44 = join4(flowsDir(), "..", "tracking.ts");
41038
+ if (!existsSync6(path44)) return null;
40057
41039
  try {
40058
- const source = readFileSync12(path42, "utf-8");
41040
+ const source = readFileSync13(path44, "utf-8");
40059
41041
  const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
40060
41042
  if (!block2) return null;
40061
41043
  const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
@@ -40596,13 +41578,13 @@ function specsFromFile(parsed) {
40596
41578
  return `${destField}${type}=${entry?.value ?? ""}`;
40597
41579
  });
40598
41580
  }
40599
- function readSpecFile(path42) {
41581
+ function readSpecFile(path44) {
40600
41582
  let raw;
40601
41583
  try {
40602
- raw = path42 === "-" ? readFileSync13(0, "utf-8") : readFileSync13(path42, "utf-8");
41584
+ raw = path44 === "-" ? readFileSync14(0, "utf-8") : readFileSync14(path44, "utf-8");
40603
41585
  } catch (error) {
40604
41586
  refuse(
40605
- `Could not read ${path42 === "-" ? "the mapping from stdin" : `"${path42}"`}: ${error instanceof Error ? error.message : String(error)}`
41587
+ `Could not read ${path44 === "-" ? "the mapping from stdin" : `"${path44}"`}: ${error instanceof Error ? error.message : String(error)}`
40606
41588
  );
40607
41589
  }
40608
41590
  let parsed;
@@ -40610,7 +41592,7 @@ function readSpecFile(path42) {
40610
41592
  parsed = JSON.parse(raw);
40611
41593
  } catch (error) {
40612
41594
  refuse(
40613
- `${path42 === "-" ? "stdin" : `"${path42}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
41595
+ `${path44 === "-" ? "stdin" : `"${path44}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
40614
41596
  'Expected { "map": { "<destField>": "<value>", \u2026 } }'
40615
41597
  );
40616
41598
  }
@@ -40880,18 +41862,18 @@ var ARRAY_FIELDS = [
40880
41862
  "tagIds"
40881
41863
  ];
40882
41864
  var ARRAY_OWNERS = ["", "body"];
40883
- function dropUnsetOptionals(sideEffect, path42) {
41865
+ function dropUnsetOptionals(sideEffect, path44) {
40884
41866
  return OPTIONAL_STRINGS.flatMap((key) => {
40885
41867
  if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
40886
41868
  delete sideEffect[key];
40887
- return [{ path: path42, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
41869
+ return [{ path: path44, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
40888
41870
  });
40889
41871
  }
40890
- function fillNulledArrays(target, prefix, path42) {
41872
+ function fillNulledArrays(target, prefix, path44) {
40891
41873
  return ARRAY_FIELDS.flatMap((key) => {
40892
41874
  if (!(key in target) || target[key] !== null) return [];
40893
41875
  target[key] = [];
40894
- return [{ path: path42, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
41876
+ return [{ path: path44, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
40895
41877
  });
40896
41878
  }
40897
41879
  function sideEffectsOf(node) {
@@ -40901,13 +41883,13 @@ function sideEffectsOf(node) {
40901
41883
  );
40902
41884
  }
40903
41885
  function normalizeSideEffect(sideEffect, where) {
40904
- const path42 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
41886
+ const path44 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
40905
41887
  const arrays = ARRAY_OWNERS.flatMap((owner) => {
40906
41888
  const target = owner ? sideEffect[owner] : sideEffect;
40907
41889
  if (!target || typeof target !== "object") return [];
40908
- return fillNulledArrays(target, owner ? `${owner}.` : "", path42);
41890
+ return fillNulledArrays(target, owner ? `${owner}.` : "", path44);
40909
41891
  });
40910
- return [...dropUnsetOptionals(sideEffect, path42), ...arrays];
41892
+ return [...dropUnsetOptionals(sideEffect, path44), ...arrays];
40911
41893
  }
40912
41894
  function normalizeFlowTree(tree) {
40913
41895
  const changes = [];
@@ -41140,9 +42122,15 @@ var listCommand13 = defineCommand124({
41140
42122
  writeJson({
41141
42123
  ok: true,
41142
42124
  data: { flows },
41143
- hints: pending.length > 0 ? [
41144
- `Still waiting on the user: ${pending.map((flow) => `${flow.slug} (${flow.needsConfig})`).join(", ")}. \`baker flows show <slug>\` returns the request_flow_input call for each.`
41145
- ] : []
42125
+ hints: [
42126
+ ...pending.length > 0 ? [
42127
+ `Still waiting on the user: ${pending.map((flow) => `${flow.slug} (${flow.needsConfig})`).join(", ")}. \`baker flows show <slug>\` returns the request_flow_input call for each.`
42128
+ ] : [],
42129
+ // "Set the conversions on the Forms" lands here first, and nothing in
42130
+ // this family can do it — the conversion side effects it does offer
42131
+ // fire a pixel and are not what any Baker report counts.
42132
+ "Setting what a Form COUNTS is not in this family: a conversion is a named event held in Baker. `baker analytics conversions` lists what this company counts; `--flow <slug>` spells one Form's endings as keys you can count, traffic or no traffic."
42133
+ ]
41146
42134
  });
41147
42135
  }
41148
42136
  });
@@ -41170,7 +42158,15 @@ var showCommand4 = defineCommand124({
41170
42158
  data: response,
41171
42159
  // Inert side effects first: an unconfigured destination is work still to
41172
42160
  // do, but a side effect wired to the wrong moment reads as done.
41173
- hints: [...inertSideEffectHints(tree), ...pendingInputHints(slug, config)]
42161
+ hints: [
42162
+ ...inertSideEffectHints(tree),
42163
+ ...pendingInputHints(slug, config),
42164
+ // The question this command is asked — "is this Form wired up?" — has
42165
+ // an answer that is not in this file at all. What a Form counts is a
42166
+ // named event in Baker, and a Form can be perfectly configured, fire
42167
+ // every pixel it has, and still report converting nobody.
42168
+ `What this Form counts as a conversion is NOT in _data.json and not in any side effect here: it is a named event held in Baker. Read it, and this Form's own endings spelled as event keys, with \`baker analytics conversions --flow ${slug}\`.`
42169
+ ]
41174
42170
  });
41175
42171
  }
41176
42172
  });
@@ -41335,7 +42331,7 @@ Examples:
41335
42331
  import { defineCommand as defineCommand126 } from "citty";
41336
42332
 
41337
42333
  // src/commands/ga4/shared.ts
41338
- import { readFileSync as readFileSync14 } from "fs";
42334
+ import { readFileSync as readFileSync15 } from "fs";
41339
42335
  function failValidation3(message) {
41340
42336
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
41341
42337
  process.exit(1);
@@ -41351,7 +42347,7 @@ function requireTarget5(args, entity) {
41351
42347
  function readJsonSource(args, flag) {
41352
42348
  const inline = args[flag];
41353
42349
  const file = args.file;
41354
- const raw = typeof file === "string" && file.length > 0 ? readFileSync14(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
42350
+ const raw = typeof file === "string" && file.length > 0 ? readFileSync15(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
41355
42351
  if (raw === void 0) {
41356
42352
  failValidation3(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
41357
42353
  }
@@ -41445,10 +42441,10 @@ async function stageOps(ops) {
41445
42441
  handleError2(err);
41446
42442
  }
41447
42443
  }
41448
- async function draftAction2(path42, body, chat) {
42444
+ async function draftAction2(path44, body, chat) {
41449
42445
  const chatId = resolveChatId(chat);
41450
42446
  try {
41451
- const data = await apiPost(path42, { chatId, ...body });
42447
+ const data = await apiPost(path44, { chatId, ...body });
41452
42448
  writeJsonEnvelope({ ok: true, data });
41453
42449
  return data;
41454
42450
  } catch (err) {
@@ -41702,7 +42698,7 @@ Examples:
41702
42698
  });
41703
42699
 
41704
42700
  // src/commands/ga4/query.ts
41705
- import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
42701
+ import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync4 } from "fs";
41706
42702
  import { resolve as resolve2 } from "path";
41707
42703
  import { defineCommand as defineCommand129 } from "citty";
41708
42704
 
@@ -41793,7 +42789,7 @@ function writeRowsToFile2(filePath, rows, append) {
41793
42789
  writeFileSync4(filePath, content, "utf-8");
41794
42790
  }
41795
42791
  } else if (append && existsSync7(filePath)) {
41796
- const existing = JSON.parse(readFileSync15(filePath, "utf-8"));
42792
+ const existing = JSON.parse(readFileSync16(filePath, "utf-8"));
41797
42793
  writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
41798
42794
  } else {
41799
42795
  writeFileSync4(filePath, JSON.stringify(rows, null, 2), "utf-8");
@@ -42645,7 +43641,7 @@ Full guide: __tooling__/docs/tools/baker/ga4.md`
42645
43641
  import { defineCommand as defineCommand135 } from "citty";
42646
43642
 
42647
43643
  // src/commands/gsc/query.ts
42648
- import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync16, writeFileSync as writeFileSync5 } from "fs";
43644
+ import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
42649
43645
  import { resolve as resolve3 } from "path";
42650
43646
  import { defineCommand as defineCommand132 } from "citty";
42651
43647
 
@@ -42793,7 +43789,7 @@ function writeRowsToFile3(filePath, rows, append) {
42793
43789
  writeFileSync5(filePath, content, "utf-8");
42794
43790
  }
42795
43791
  } else if (append && existsSync8(filePath)) {
42796
- const existing = JSON.parse(readFileSync16(filePath, "utf-8"));
43792
+ const existing = JSON.parse(readFileSync17(filePath, "utf-8"));
42797
43793
  writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
42798
43794
  } else {
42799
43795
  writeFileSync5(filePath, JSON.stringify(rows, null, 2), "utf-8");
@@ -43170,7 +44166,7 @@ var listCommand14 = defineCommand136({
43170
44166
  }
43171
44167
  }
43172
44168
  });
43173
- var historyCommand = defineCommand136({
44169
+ var historyCommand2 = defineCommand136({
43174
44170
  meta: {
43175
44171
  name: "history",
43176
44172
  description: `Unified account history (audit log): what changed, who did it, and when.
@@ -43862,7 +44858,7 @@ function cropSprite(input, region) {
43862
44858
 
43863
44859
  // src/lib/image/io.ts
43864
44860
  import { randomBytes } from "crypto";
43865
- import { glob as fsGlob, readFile as readFile22, rename, stat as stat6, writeFile as writeFile12 } from "fs/promises";
44861
+ import { glob as fsGlob, readFile as readFile24, rename, stat as stat6, writeFile as writeFile12 } from "fs/promises";
43866
44862
  import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
43867
44863
  var REMOTE_RE = /^https?:\/\//i;
43868
44864
  var GLOB_RE = /[*?[\]{}]/;
@@ -43895,11 +44891,11 @@ async function readImageBuffer(pathOrUrl) {
43895
44891
  const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
43896
44892
  return buffer;
43897
44893
  }
43898
- return readFile22(pathOrUrl);
44894
+ return readFile24(pathOrUrl);
43899
44895
  }
43900
- async function isDirectory(path42) {
44896
+ async function isDirectory(path44) {
43901
44897
  try {
43902
- const s = await stat6(path42);
44898
+ const s = await stat6(path44);
43903
44899
  return s.isDirectory();
43904
44900
  } catch {
43905
44901
  return false;
@@ -44218,13 +45214,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
44218
45214
  }
44219
45215
  function disambiguate(paths) {
44220
45216
  const taken = /* @__PURE__ */ new Set();
44221
- return paths.map((path42) => {
44222
- if (!taken.has(path42)) {
44223
- taken.add(path42);
44224
- return path42;
45217
+ return paths.map((path44) => {
45218
+ if (!taken.has(path44)) {
45219
+ taken.add(path44);
45220
+ return path44;
44225
45221
  }
44226
- const ext = extname3(path42);
44227
- const stem = path42.slice(0, path42.length - ext.length);
45222
+ const ext = extname3(path44);
45223
+ const stem = path44.slice(0, path44.length - ext.length);
44228
45224
  let n = 2;
44229
45225
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
44230
45226
  const unique = `${stem}-${n}${ext}`;
@@ -44350,10 +45346,10 @@ async function runDownloads(plan) {
44350
45346
  const paths = disambiguate(fetched.map((item) => item.path));
44351
45347
  const downloaded = [];
44352
45348
  for (const [index, item] of fetched.entries()) {
44353
- const path42 = paths[index] ?? item.path;
45349
+ const path44 = paths[index] ?? item.path;
44354
45350
  try {
44355
- await atomicWrite(path42, item.buffer);
44356
- downloaded.push({ input: item.input, output: path42, bytes: item.buffer.length, contentType: item.contentType });
45351
+ await atomicWrite(path44, item.buffer);
45352
+ downloaded.push({ input: item.input, output: path44, bytes: item.buffer.length, contentType: item.contentType });
44357
45353
  } catch (err) {
44358
45354
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
44359
45355
  }
@@ -47317,8 +48313,8 @@ Full guide: __tooling__/docs/tools/baker/images.md`
47317
48313
  import { defineCommand as defineCommand175 } from "citty";
47318
48314
 
47319
48315
  // src/commands/landing/critique.ts
47320
- import { readdir as readdir10, readFile as readFile24, stat as stat7 } from "fs/promises";
47321
- import path31 from "path";
48316
+ import { readdir as readdir12, readFile as readFile26, stat as stat8 } from "fs/promises";
48317
+ import path33 from "path";
47322
48318
  import { defineCommand as defineCommand164 } from "citty";
47323
48319
 
47324
48320
  // src/engine/landing/lib/constants.ts
@@ -48496,13 +49492,13 @@ function describeCounts(findings) {
48496
49492
 
48497
49493
  // src/commands/landing/snapshot.ts
48498
49494
  import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
48499
- import path29 from "path";
49495
+ import path31 from "path";
48500
49496
  var CRITIC_VERSION = "3";
48501
49497
  function critiqueCacheDir(projectRoot) {
48502
- return path29.join(projectRoot, ".cache", "landing-critique");
49498
+ return path31.join(projectRoot, ".cache", "landing-critique");
48503
49499
  }
48504
49500
  function snapshotPath(projectRoot, slug) {
48505
- return path29.join(critiqueCacheDir(projectRoot), `${slug}.json`);
49501
+ return path31.join(critiqueCacheDir(projectRoot), `${slug}.json`);
48506
49502
  }
48507
49503
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
48508
49504
  await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
@@ -48514,8 +49510,8 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
48514
49510
  }
48515
49511
 
48516
49512
  // src/commands/landing/source-version.ts
48517
- import { readFile as readFile23 } from "fs/promises";
48518
- import path30 from "path";
49513
+ import { readdir as readdir11, readFile as readFile25, stat as stat7 } from "fs/promises";
49514
+ import path32 from "path";
48519
49515
  var CRITIQUED_ROOTS = ["src/pages/", "src/components/"];
48520
49516
  async function landingSourceRelPaths(root, slug) {
48521
49517
  const files = await landingGraphFiles(root, slug);
@@ -48524,7 +49520,7 @@ async function landingSourceRelPaths(root, slug) {
48524
49520
  async function readLandingSources(root, slug) {
48525
49521
  const rel = await landingSourceRelPaths(root, slug);
48526
49522
  const out = [];
48527
- for (const r of rel) out.push({ path: r, text: await readFile23(path30.join(root, r), "utf8") });
49523
+ for (const r of rel) out.push({ path: r, text: await readFile25(path32.join(root, r), "utf8") });
48528
49524
  return out;
48529
49525
  }
48530
49526
  async function computeLandingSourceSha(root, slug) {
@@ -48533,7 +49529,7 @@ async function computeLandingSourceSha(root, slug) {
48533
49529
  for (const r of rel) {
48534
49530
  let bytes;
48535
49531
  try {
48536
- bytes = await readFile23(path30.join(root, r));
49532
+ bytes = await readFile25(path32.join(root, r));
48537
49533
  } catch {
48538
49534
  bytes = Buffer.alloc(0);
48539
49535
  }
@@ -48541,6 +49537,47 @@ async function computeLandingSourceSha(root, slug) {
48541
49537
  }
48542
49538
  return sha256Hex(Buffer.concat(parts));
48543
49539
  }
49540
+ async function computeLegacyLandingSourceSha(landingDir) {
49541
+ const rel = [];
49542
+ if (await isFile2(path32.join(landingDir, "index.astro"))) rel.push("index.astro");
49543
+ for (const abs of await walkAstro(path32.join(landingDir, "_components"))) {
49544
+ rel.push(path32.relative(landingDir, abs).split(path32.sep).join("/"));
49545
+ }
49546
+ rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
49547
+ const parts = [];
49548
+ for (const r of rel) {
49549
+ let bytes;
49550
+ try {
49551
+ bytes = await readFile25(path32.join(landingDir, r));
49552
+ } catch {
49553
+ bytes = Buffer.alloc(0);
49554
+ }
49555
+ parts.push(Buffer.from(`${r}\0${bytes.length}\0`), bytes);
49556
+ }
49557
+ return sha256Hex(Buffer.concat(parts));
49558
+ }
49559
+ async function isFile2(p) {
49560
+ try {
49561
+ return (await stat7(p)).isFile();
49562
+ } catch {
49563
+ return false;
49564
+ }
49565
+ }
49566
+ async function walkAstro(dir) {
49567
+ let entries;
49568
+ try {
49569
+ entries = await readdir11(dir, { withFileTypes: true });
49570
+ } catch {
49571
+ return [];
49572
+ }
49573
+ const out = [];
49574
+ for (const entry of entries) {
49575
+ const abs = path32.join(dir, entry.name);
49576
+ if (entry.isDirectory()) out.push(...await walkAstro(abs));
49577
+ else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
49578
+ }
49579
+ return out;
49580
+ }
48544
49581
 
48545
49582
  // src/commands/landing/critique.ts
48546
49583
  registerSchema({
@@ -48596,7 +49633,7 @@ var critiqueCommand2 = defineCommand164({
48596
49633
  { availableSlugs: await listLandingSlugs(projectRoot) }
48597
49634
  );
48598
49635
  }
48599
- if (!await isDir2(path31.resolve(projectRoot, "src", "pages", slug))) {
49636
+ if (!await isDir2(path33.resolve(projectRoot, "src", "pages", slug))) {
48600
49637
  fail6("NOT_FOUND", `No landing at src/pages/${slug}/`, {
48601
49638
  availableSlugs: await listLandingSlugs(projectRoot)
48602
49639
  });
@@ -48636,15 +49673,17 @@ var critiqueCommand2 = defineCommand164({
48636
49673
  }
48637
49674
  });
48638
49675
  async function critiqueOne(projectRoot, slug, brand, competitors) {
48639
- const [sources, sourceSha] = await Promise.all([
49676
+ const [sources, compositionSha, sourceSha] = await Promise.all([
48640
49677
  readLandingSources(projectRoot, slug),
48641
- computeLandingSourceSha(projectRoot, slug)
49678
+ computeLandingSourceSha(projectRoot, slug),
49679
+ computeLegacyLandingSourceSha(path33.resolve(projectRoot, "src", "pages", slug))
48642
49680
  ]);
48643
49681
  const report = critiqueLanding({ slug, sources, brand, competitors });
48644
49682
  let snapshotFailed = false;
48645
49683
  try {
48646
49684
  await writeCritiqueSnapshot(projectRoot, {
48647
49685
  slug,
49686
+ compositionSha,
48648
49687
  sourceSha,
48649
49688
  criticVersion: CRITIC_VERSION,
48650
49689
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -48657,18 +49696,18 @@ async function critiqueOne(projectRoot, slug, brand, competitors) {
48657
49696
  return { slug, report, snapshotFailed };
48658
49697
  }
48659
49698
  async function readCompetitorNames(projectRoot) {
48660
- const dir = path31.join(projectRoot, "src", "content", "competitors");
49699
+ const dir = path33.join(projectRoot, "src", "content", "competitors");
48661
49700
  let entries;
48662
49701
  try {
48663
- entries = (await readdir10(dir)).filter((f) => f.endsWith(".md"));
49702
+ entries = (await readdir12(dir)).filter((f) => f.endsWith(".md"));
48664
49703
  } catch {
48665
49704
  return [];
48666
49705
  }
48667
49706
  const names = [];
48668
49707
  for (const entry of entries) {
48669
- names.push(path31.basename(entry, ".md").replace(/[-_]+/g, " "));
49708
+ names.push(path33.basename(entry, ".md").replace(/[-_]+/g, " "));
48670
49709
  try {
48671
- const head = (await readFile24(path31.join(dir, entry), "utf8")).slice(0, 2e3);
49710
+ const head = (await readFile26(path33.join(dir, entry), "utf8")).slice(0, 2e3);
48672
49711
  const titled = /^\s*(?:title|name)\s*:\s*["']?([^"'\n]+)["']?\s*$/im.exec(head);
48673
49712
  if (titled?.[1]) names.push(titled[1].trim());
48674
49713
  } catch {
@@ -48678,7 +49717,7 @@ async function readCompetitorNames(projectRoot) {
48678
49717
  }
48679
49718
  async function listLandingSlugs(projectRoot) {
48680
49719
  try {
48681
- const entries = await readdir10(path31.join(projectRoot, "src", "pages"), { withFileTypes: true });
49720
+ const entries = await readdir12(path33.join(projectRoot, "src", "pages"), { withFileTypes: true });
48682
49721
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
48683
49722
  } catch {
48684
49723
  return [];
@@ -48697,7 +49736,7 @@ function present(f) {
48697
49736
  }
48698
49737
  async function isDir2(p) {
48699
49738
  try {
48700
- return (await stat7(p)).isDirectory();
49739
+ return (await stat8(p)).isDirectory();
48701
49740
  } catch {
48702
49741
  return false;
48703
49742
  }
@@ -48830,7 +49869,7 @@ var addCommand = defineCommand165({
48830
49869
 
48831
49870
  // src/commands/landing/inspiration/code.ts
48832
49871
  import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
48833
- import path32 from "path";
49872
+ import path34 from "path";
48834
49873
  import { defineCommand as defineCommand166 } from "citty";
48835
49874
  registerSchema({
48836
49875
  command: "landing.inspiration.code",
@@ -48853,9 +49892,9 @@ var codeCommand = defineCommand166({
48853
49892
  try {
48854
49893
  const id = args.id;
48855
49894
  const data = await apiGet("/api/landing-inspiration/section-code", { id });
48856
- const dir = path32.join(process.cwd(), ".baker", "inspiration", id);
49895
+ const dir = path34.join(process.cwd(), ".baker", "inspiration", id);
48857
49896
  await mkdir10(dir, { recursive: true });
48858
- const file = path32.join(dir, "section.html");
49897
+ const file = path34.join(dir, "section.html");
48859
49898
  await writeFile14(file, data.html);
48860
49899
  const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
48861
49900
  const fidelity = fidelityHint(data.fidelity);
@@ -48865,7 +49904,7 @@ var codeCommand = defineCommand166({
48865
49904
  ok: true,
48866
49905
  data: {
48867
49906
  id,
48868
- file: path32.relative(process.cwd(), file),
49907
+ file: path34.relative(process.cwd(), file),
48869
49908
  bytes: data.html.length,
48870
49909
  fidelity: data.fidelity,
48871
49910
  reproduction_notes: data.reproductionNotes,
@@ -49297,7 +50336,7 @@ function classifyCaptureFailure(error) {
49297
50336
 
49298
50337
  // src/engine/landing-library/run.ts
49299
50338
  import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
49300
- import path34 from "path";
50339
+ import path36 from "path";
49301
50340
 
49302
50341
  // ../proxy/src/preflight.ts
49303
50342
  import http from "http";
@@ -49436,7 +50475,7 @@ var inPageCollectUsedCss = (selector) => {
49436
50475
  }
49437
50476
  return false;
49438
50477
  };
49439
- const walk = (rules, sink) => {
50478
+ const walk2 = (rules, sink) => {
49440
50479
  for (const rule of Array.from(rules)) {
49441
50480
  totalRules++;
49442
50481
  if (rule instanceof CSSStyleRule) {
@@ -49457,7 +50496,7 @@ var inPageCollectUsedCss = (selector) => {
49457
50496
  const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
49458
50497
  if (grouping) {
49459
50498
  const inner = [];
49460
- walk(rule.cssRules, inner);
50499
+ walk2(rule.cssRules, inner);
49461
50500
  if (inner.length === 0) continue;
49462
50501
  const condition = rule.conditionText ?? "";
49463
50502
  const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
@@ -49471,7 +50510,7 @@ ${inner.join("\n")}
49471
50510
  const owner = sheet.ownerNode;
49472
50511
  if (owner?.hasAttribute?.("data-baker-freeze")) continue;
49473
50512
  try {
49474
- walk(sheet.cssRules, kept);
50513
+ walk2(sheet.cssRules, kept);
49475
50514
  } catch {
49476
50515
  if (sheet.href) unreadableHrefs.push(sheet.href);
49477
50516
  }
@@ -49922,7 +50961,7 @@ var inPageCollectMotion = (selector) => {
49922
50961
  else if (infinite) loop.push(effect);
49923
50962
  else entrance.push(effect);
49924
50963
  };
49925
- const walk = (rules, insideScrollTimeline) => {
50964
+ const walk2 = (rules, insideScrollTimeline) => {
49926
50965
  for (const rule of Array.from(rules)) {
49927
50966
  if (rule instanceof CSSKeyframesRule) {
49928
50967
  keyframesByName.set(rule.name, rule.cssText);
@@ -49940,17 +50979,17 @@ var inPageCollectMotion = (selector) => {
49940
50979
  respectsReducedMotion = true;
49941
50980
  continue;
49942
50981
  }
49943
- walk(rule.cssRules, insideScrollTimeline);
50982
+ walk2(rule.cssRules, insideScrollTimeline);
49944
50983
  continue;
49945
50984
  }
49946
50985
  const grouping = rule;
49947
- if (grouping.cssRules) walk(grouping.cssRules, insideScrollTimeline);
50986
+ if (grouping.cssRules) walk2(grouping.cssRules, insideScrollTimeline);
49948
50987
  }
49949
50988
  };
49950
50989
  for (const sheet of Array.from(document.styleSheets)) {
49951
50990
  if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
49952
50991
  try {
49953
- walk(sheet.cssRules, false);
50992
+ walk2(sheet.cssRules, false);
49954
50993
  } catch {
49955
50994
  }
49956
50995
  }
@@ -50713,9 +51752,9 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
50713
51752
 
50714
51753
  // src/engine/landing-library/report.ts
50715
51754
  import { writeFile as writeFile15 } from "fs/promises";
50716
- import path33 from "path";
51755
+ import path35 from "path";
50717
51756
  async function writeCaptureReport(manifest, outDir) {
50718
- const file = path33.join(outDir, "report.html");
51757
+ const file = path35.join(outDir, "report.html");
50719
51758
  await writeFile15(file, renderReport(manifest));
50720
51759
  return file;
50721
51760
  }
@@ -50894,32 +51933,32 @@ async function reproducePage(args) {
50894
51933
  const { browser, page, outDir, pageUrl, livePageShot } = args;
50895
51934
  const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
50896
51935
  if (!built) return { bundle: null, fidelity: null };
50897
- await writeFile16(path34.join(outDir, "page.html"), built.html);
51936
+ await writeFile16(path36.join(outDir, "page.html"), built.html);
50898
51937
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
50899
51938
  wholePage: true,
50900
51939
  timeoutMs: 6e4
50901
51940
  });
50902
51941
  if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
50903
- await writeFile16(path34.join(outDir, "page-rendered.png"), rendered);
51942
+ await writeFile16(path36.join(outDir, "page-rendered.png"), rendered);
50904
51943
  const { score, note } = await scoreFidelity(livePageShot, rendered);
50905
51944
  return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
50906
51945
  }
50907
51946
  async function captureOneSection(args) {
50908
51947
  const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
50909
- const dir = path34.join(sectionsDir, String(candidate.index).padStart(2, "0"));
51948
+ const dir = path36.join(sectionsDir, String(candidate.index).padStart(2, "0"));
50910
51949
  await mkdir11(dir, { recursive: true });
50911
51950
  const desktop = await captureSection(page, candidate);
50912
- if (desktop) await writeFile16(path34.join(dir, "desktop.png"), desktop);
51951
+ if (desktop) await writeFile16(path36.join(dir, "desktop.png"), desktop);
50913
51952
  const visualHash = desktop ? await perceptualHash(desktop) : null;
50914
51953
  const motion = await collectMotion(page, candidate.selector);
50915
51954
  const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
50916
51955
  let fidelity = null;
50917
51956
  let fidelityNote;
50918
51957
  if (built) {
50919
- await writeFile16(path34.join(dir, "section.html"), built.html);
51958
+ await writeFile16(path36.join(dir, "section.html"), built.html);
50920
51959
  const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
50921
51960
  if (rendered && desktop) {
50922
- await writeFile16(path34.join(dir, "section-rendered.png"), rendered);
51961
+ await writeFile16(path36.join(dir, "section-rendered.png"), rendered);
50923
51962
  const result = await scoreFidelity(desktop, rendered);
50924
51963
  fidelity = result.score;
50925
51964
  fidelityNote = result.note;
@@ -50927,9 +51966,9 @@ async function captureOneSection(args) {
50927
51966
  }
50928
51967
  return {
50929
51968
  ...candidate,
50930
- desktopShot: desktop ? path34.relative(outDir, path34.join(dir, "desktop.png")) : null,
51969
+ desktopShot: desktop ? path36.relative(outDir, path36.join(dir, "desktop.png")) : null,
50931
51970
  mobileShot: null,
50932
- bundle: built ? path34.relative(outDir, path34.join(dir, "section.html")) : null,
51971
+ bundle: built ? path36.relative(outDir, path36.join(dir, "section.html")) : null,
50933
51972
  fidelity,
50934
51973
  ...fidelityNote ? { fidelityNote } : {},
50935
51974
  ...built ? { cssStats: built.stats } : {},
@@ -50948,9 +51987,9 @@ async function captureMobileShots(args) {
50948
51987
  for (const section of sections) {
50949
51988
  const shot = await captureSectionOnMobile(mobile.page, section);
50950
51989
  if (!shot) continue;
50951
- const file = path34.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
51990
+ const file = path36.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
50952
51991
  await writeFile16(file, shot);
50953
- section.mobileShot = path34.relative(outDir, file);
51992
+ section.mobileShot = path36.relative(outDir, file);
50954
51993
  }
50955
51994
  } finally {
50956
51995
  await mobile.context.close();
@@ -50965,10 +52004,10 @@ async function captureMotionTakes(args) {
50965
52004
  const filmOne = async (section) => {
50966
52005
  const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
50967
52006
  if (!take) return;
50968
- const dir = path34.join(sectionsDir, String(section.index).padStart(2, "0"));
50969
- const file = path34.join(dir, "motion-filmstrip.png");
52007
+ const dir = path36.join(sectionsDir, String(section.index).padStart(2, "0"));
52008
+ const file = path36.join(dir, "motion-filmstrip.png");
50970
52009
  await writeFile16(file, take.filmstrip);
50971
- section.motionFilmstrip = path34.relative(outDir, file);
52010
+ section.motionFilmstrip = path36.relative(outDir, file);
50972
52011
  log(` [${section.index}] ${section.motion.summary}`);
50973
52012
  };
50974
52013
  const queue = [...moving];
@@ -51025,7 +52064,7 @@ async function captureAlternateViews(args) {
51025
52064
  async function reproduceWholePage(args) {
51026
52065
  const { browser, page, outDir, pageUrl, withCode, log } = args;
51027
52066
  const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
51028
- if (fullPage) await writeFile16(path34.join(outDir, "full-page.png"), fullPage);
52067
+ if (fullPage) await writeFile16(path36.join(outDir, "full-page.png"), fullPage);
51029
52068
  if (!withCode) return { bundle: null, fidelity: null };
51030
52069
  const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
51031
52070
  log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
@@ -51096,7 +52135,7 @@ async function openViaLadder(args) {
51096
52135
  async function scrapeLanding(options) {
51097
52136
  const timeoutMs = options.timeoutMs ?? 45e3;
51098
52137
  const log = options.onProgress ?? (() => void 0);
51099
- const sectionsDir = path34.join(options.outDir, "sections");
52138
+ const sectionsDir = path36.join(options.outDir, "sections");
51100
52139
  const nonPublic = refuseNonPublicUrl(options.url);
51101
52140
  if (nonPublic) {
51102
52141
  throw new BlockedPageError({
@@ -51158,7 +52197,7 @@ async function scrapeLanding(options) {
51158
52197
  security: prepared.security,
51159
52198
  captureTier: tier
51160
52199
  };
51161
- await writeFile16(path34.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
52200
+ await writeFile16(path36.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
51162
52201
  `);
51163
52202
  if (options.report !== false) {
51164
52203
  const reportPath = await writeCaptureReport(manifest, options.outDir);
@@ -51173,28 +52212,28 @@ async function scrapeLanding(options) {
51173
52212
 
51174
52213
  // src/commands/landing/inspiration/captureOut.ts
51175
52214
  import { existsSync as existsSync9 } from "fs";
51176
- import path35 from "path";
52215
+ import path37 from "path";
51177
52216
  var SCRATCH_DIR = ".baker";
51178
52217
  function isWithin(parent, target) {
51179
- const relative = path35.relative(parent, target);
51180
- return relative === "" || !relative.startsWith("..") && !path35.isAbsolute(relative);
52218
+ const relative = path37.relative(parent, target);
52219
+ return relative === "" || !relative.startsWith("..") && !path37.isAbsolute(relative);
51181
52220
  }
51182
52221
  function findRepoRoot(from) {
51183
- let dir = path35.resolve(from);
52222
+ let dir = path37.resolve(from);
51184
52223
  for (; ; ) {
51185
- if (existsSync9(path35.join(dir, ".git"))) return dir;
51186
- const parent = path35.dirname(dir);
52224
+ if (existsSync9(path37.join(dir, ".git"))) return dir;
52225
+ const parent = path37.dirname(dir);
51187
52226
  if (parent === dir) return null;
51188
52227
  dir = parent;
51189
52228
  }
51190
52229
  }
51191
52230
  function checkCaptureOut(out, options) {
51192
52231
  const { cwd, repoRoot } = options;
51193
- const resolved = path35.resolve(cwd, out);
52232
+ const resolved = path37.resolve(cwd, out);
51194
52233
  if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
51195
- const scratch = path35.join(repoRoot, SCRATCH_DIR);
52234
+ const scratch = path37.join(repoRoot, SCRATCH_DIR);
51196
52235
  if (isWithin(scratch, resolved)) return { ok: true };
51197
- const suggestion = path35.posix.join(SCRATCH_DIR, "teardowns", path35.basename(resolved) || "capture");
52236
+ const suggestion = path37.posix.join(SCRATCH_DIR, "teardowns", path37.basename(resolved) || "capture");
51198
52237
  return {
51199
52238
  ok: false,
51200
52239
  error: {
@@ -51368,12 +52407,12 @@ var scrapeCommand = defineCommand169({
51368
52407
  });
51369
52408
 
51370
52409
  // src/commands/landing/inspiration/search.ts
51371
- import path37 from "path";
52410
+ import path39 from "path";
51372
52411
  import { defineCommand as defineCommand170 } from "citty";
51373
52412
 
51374
52413
  // src/commands/landing/inspiration/shot.ts
51375
52414
  import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
51376
- import path36 from "path";
52415
+ import path38 from "path";
51377
52416
  import sharp6 from "sharp";
51378
52417
  var READABLE_SHOT = {
51379
52418
  maxWidth: 1440,
@@ -51399,9 +52438,9 @@ async function downloadReadableShot(url, file) {
51399
52438
  const response = await fetch(url);
51400
52439
  if (!response.ok) return null;
51401
52440
  const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
51402
- await mkdir12(path36.dirname(file), { recursive: true });
52441
+ await mkdir12(path38.dirname(file), { recursive: true });
51403
52442
  await writeFile17(file, shot);
51404
- return path36.relative(process.cwd(), file);
52443
+ return path38.relative(process.cwd(), file);
51405
52444
  } catch {
51406
52445
  return null;
51407
52446
  }
@@ -51493,13 +52532,13 @@ function buildSearchBody(args) {
51493
52532
  return body;
51494
52533
  }
51495
52534
  async function downloadShots(results) {
51496
- const dir = path37.join(process.cwd(), ".baker", "inspiration");
52535
+ const dir = path39.join(process.cwd(), ".baker", "inspiration");
51497
52536
  const saved = /* @__PURE__ */ new Map();
51498
52537
  await Promise.all(
51499
52538
  results.map(async (result) => {
51500
52539
  const file = await downloadReadableShot(
51501
52540
  result.desktopShotUrl,
51502
- path37.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
52541
+ path39.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
51503
52542
  );
51504
52543
  if (file) saved.set(result.id, file);
51505
52544
  })
@@ -51727,7 +52766,7 @@ var sequencesCommand = defineCommand171({
51727
52766
  });
51728
52767
 
51729
52768
  // src/commands/landing/inspiration/view.ts
51730
- import path38 from "path";
52769
+ import path40 from "path";
51731
52770
  import { defineCommand as defineCommand172 } from "citty";
51732
52771
  registerSchema({
51733
52772
  command: "landing.inspiration.view",
@@ -51760,12 +52799,12 @@ var viewCommand2 = defineCommand172({
51760
52799
  const id = args.id;
51761
52800
  const data = await apiGet("/api/landing-inspiration/section", { id });
51762
52801
  const section = data.section;
51763
- const dir = path38.join(process.cwd(), ".baker", "inspiration", id);
52802
+ const dir = path40.join(process.cwd(), ".baker", "inspiration", id);
51764
52803
  const ext = READABLE_SHOT.extension;
51765
52804
  const [desktop, mobile, filmstrip] = await Promise.all([
51766
- downloadReadableShot(section.desktopShotUrl, path38.join(dir, `desktop.${ext}`)),
51767
- downloadReadableShot(section.mobileShotUrl, path38.join(dir, `mobile.${ext}`)),
51768
- downloadReadableShot(section.motionFilmstripUrl, path38.join(dir, `motion-filmstrip.${ext}`))
52805
+ downloadReadableShot(section.desktopShotUrl, path40.join(dir, `desktop.${ext}`)),
52806
+ downloadReadableShot(section.mobileShotUrl, path40.join(dir, `mobile.${ext}`)),
52807
+ downloadReadableShot(section.motionFilmstripUrl, path40.join(dir, `motion-filmstrip.${ext}`))
51769
52808
  ]);
51770
52809
  const full = args.full;
51771
52810
  const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
@@ -51871,8 +52910,8 @@ Full guide: __tooling__/docs/tools/baker/landing.md`
51871
52910
 
51872
52911
  // src/commands/landing/variant.ts
51873
52912
  import { randomUUID as randomUUID2 } from "crypto";
51874
- import { mkdir as mkdir13, readdir as readdir11, readFile as readFile25, stat as stat8, writeFile as writeFile18 } from "fs/promises";
51875
- import path39 from "path";
52913
+ import { mkdir as mkdir13, readdir as readdir13, readFile as readFile27, stat as stat9, writeFile as writeFile18 } from "fs/promises";
52914
+ import path41 from "path";
51876
52915
  import { defineCommand as defineCommand174 } from "citty";
51877
52916
  registerSchema({
51878
52917
  command: "landing.variant",
@@ -51899,7 +52938,7 @@ registerSchema({
51899
52938
  var SLUG_RE2 = /^[a-z0-9][a-z0-9-]*$/;
51900
52939
  async function readInternalId(definitionPath) {
51901
52940
  try {
51902
- const text2 = await readFile25(definitionPath, "utf8");
52941
+ const text2 = await readFile27(definitionPath, "utf8");
51903
52942
  return /^internalId:\s*"?([^"\s]+)"?\s*$/m.exec(text2)?.[1] ?? null;
51904
52943
  } catch {
51905
52944
  return null;
@@ -51908,7 +52947,7 @@ async function readInternalId(definitionPath) {
51908
52947
  async function archivedVariantNumbers(root, internalId) {
51909
52948
  if (!internalId) return [];
51910
52949
  try {
51911
- const entries = await readdir11(path39.resolve(root, "src/_variants", internalId), { withFileTypes: true });
52950
+ const entries = await readdir13(path41.resolve(root, "src/_variants", internalId), { withFileTypes: true });
51912
52951
  return entries.filter((entry) => entry.isDirectory()).map((entry) => Number(entry.name)).filter((n) => Number.isInteger(n) && n > 0);
51913
52952
  } catch {
51914
52953
  return [];
@@ -51923,14 +52962,14 @@ function fail7(code, message, fix) {
51923
52962
  }
51924
52963
  async function isDir3(p) {
51925
52964
  try {
51926
- return (await stat8(p)).isDirectory();
52965
+ return (await stat9(p)).isDirectory();
51927
52966
  } catch {
51928
52967
  return false;
51929
52968
  }
51930
52969
  }
51931
52970
  async function exists(p) {
51932
52971
  try {
51933
- await stat8(p);
52972
+ await stat9(p);
51934
52973
  return true;
51935
52974
  } catch {
51936
52975
  return false;
@@ -51938,7 +52977,7 @@ async function exists(p) {
51938
52977
  }
51939
52978
  async function listLandingSlugs2(root) {
51940
52979
  try {
51941
- const entries = await readdir11(path39.resolve(root, "src", "pages"), { withFileTypes: true });
52980
+ const entries = await readdir13(path41.resolve(root, "src", "pages"), { withFileTypes: true });
51942
52981
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
51943
52982
  } catch {
51944
52983
  return [];
@@ -51947,14 +52986,14 @@ async function listLandingSlugs2(root) {
51947
52986
  async function listComponents(componentsDir, prefix = "") {
51948
52987
  let entries;
51949
52988
  try {
51950
- entries = await readdir11(componentsDir, { withFileTypes: true });
52989
+ entries = await readdir13(componentsDir, { withFileTypes: true });
51951
52990
  } catch {
51952
52991
  return [];
51953
52992
  }
51954
52993
  const out = [];
51955
52994
  for (const entry of entries) {
51956
52995
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
51957
- if (entry.isDirectory()) out.push(...await listComponents(path39.join(componentsDir, entry.name), rel));
52996
+ if (entry.isDirectory()) out.push(...await listComponents(path41.join(componentsDir, entry.name), rel));
51958
52997
  else if (entry.name.endsWith(".astro")) out.push(rel);
51959
52998
  }
51960
52999
  return out.sort();
@@ -51968,27 +53007,39 @@ function resolveForkName(requested, available) {
51968
53007
  const matches = available.filter((a) => basename4(a) === cleaned || basename4(a) === `${cleaned}.astro`);
51969
53008
  return matches.length === 1 ? matches[0] ?? null : null;
51970
53009
  }
51971
- function parseForks(fork) {
51972
- const list = Array.isArray(fork) ? fork : fork === void 0 || fork === null ? [] : [fork];
51973
- const names = list.filter((s) => typeof s === "string").flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
53010
+ function parseForks(fork, rawArgs) {
53011
+ const names = repeatedValues(rawArgs, "fork", fork).flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
51974
53012
  return [...new Set(names)];
51975
53013
  }
53014
+ function pageInTest(status, page) {
53015
+ for (const test of status.experiments) {
53016
+ if (test.landingSlug !== page) continue;
53017
+ if (test.status !== "finished") {
53018
+ return `${page} is already in a test (${test.experimentId}, against ${test.variantSlug}, ${test.status}). One page has one test at a time \u2014 read it with \`baker experiment status --landing ${page}\`, and end it with \`baker experiment finish\` before building the next version.`;
53019
+ }
53020
+ if (test.fold === "owed" || test.fold === "landing") {
53021
+ return `The last test on ${page} was won by ${test.variantSlug}, and the page is still being updated to match \u2014 run \`baker experiment fold\` and publish before building the next version.`;
53022
+ }
53023
+ }
53024
+ return null;
53025
+ }
51976
53026
  function buildVariantDefinition(controlDefinition, opts) {
51977
- let out = controlDefinition;
53027
+ const frontmatter = controlDefinition.match(/^---\n[\s\S]*?\n---/)?.[0] ?? "---\n---";
53028
+ let out = frontmatter;
51978
53029
  out = out.replace(/^internalId:.*$/m, `internalId: "${opts.internalId}"`);
51979
53030
  out = out.replace(/^internalTitle:\s*(.*)$/m, (_m, title) => `internalTitle: ${title.trim()} (variant)`);
51980
53031
  const shared = opts.forks.length > 0 ? opts.forks.join(", ") : "nothing yet";
51981
53032
  const note = [
51982
53033
  "",
51983
- `## A/B test \u2014 alternative version of \`${opts.controlSlug}\``,
53034
+ `# Version ${variantNumberOf(opts.variantSlug) ?? "?"} of \`${opts.controlSlug}\` \u2014 for an A/B test`,
51984
53035
  "",
51985
- `This page is the alternative arm of a test against \`${opts.controlSlug}\`. Visitors are split at the edge and the address bar keeps saying \`/${opts.controlSlug}/\`, so both arms are the same page seen by different people.`,
53036
+ `This is not a page. It is one version of \`${opts.controlSlug}\`, shown to half of that page's visitors while a test runs; the address bar keeps saying \`/${opts.controlSlug}/\` and this folder has no address of its own. Everything about the page \u2014 the campaign, the audience, the offer, the proof \u2014 is in \`src/pages/${opts.controlSlug}/_definition.md\`, and that is where it is edited. This file only says what is different here.`,
51986
53037
  "",
51987
- `**Only this differs:** ${shared}. Every other section is imported from \`${opts.controlSlug}\`, on purpose \u2014 an edit to the control reaches both arms, so the test keeps measuring the one difference rather than a drift between two copies.`,
53038
+ `**Only this differs:** ${shared}. Every other section is imported from \`${opts.controlSlug}\`, on purpose \u2014 an edit to the page reaches both versions, so the test keeps measuring the one difference rather than a drift between two copies. Edit only the files under \`src/pages/${opts.variantSlug}/_components/\`.`,
51988
53039
  ...opts.because ? ["", `**Because:** ${opts.because}`] : [],
51989
53040
  ...opts.change ? ["", `**We changed:** ${opts.change}`] : [],
51990
53041
  "",
51991
- `Run \`baker experiment start --landing ${opts.controlSlug} --variant ${opts.variantSlug} --because "\u2026" --change "\u2026"\` to begin, and read the result with \`baker experiment status\`.`,
53042
+ `Run \`baker experiment start --landing ${opts.controlSlug} --variant ${opts.variantSlug} --because "\u2026" --change "\u2026"\` to begin, and read the result with \`baker experiment status\`. When the test ends, \`baker experiment fold\` moves the version that won into the page and removes this folder.`,
51992
53043
  ""
51993
53044
  ].join("\n");
51994
53045
  return `${out.trimEnd()}
@@ -52009,16 +53060,16 @@ async function resolveTargets(projectRoot, opts) {
52009
53060
  `"${controlSlug}" is already a variant of "${variantOfSlug(controlSlug)}". Make the next variant of the page itself: \`baker landing variant ${variantOfSlug(controlSlug)}\`.`
52010
53061
  );
52011
53062
  }
52012
- const controlDir = path39.resolve(projectRoot, "src", "pages", controlSlug);
53063
+ const controlDir = path41.resolve(projectRoot, "src", "pages", controlSlug);
52013
53064
  if (!await isDir3(controlDir)) {
52014
53065
  fail7("NOT_FOUND", `No page at src/pages/${controlSlug}/`, {
52015
53066
  availableSlugs: await listLandingSlugs2(projectRoot)
52016
53067
  });
52017
53068
  }
52018
- if (!await exists(path39.join(controlDir, "index.astro"))) {
53069
+ if (!await exists(path41.join(controlDir, "index.astro"))) {
52019
53070
  fail7("NOT_FOUND", `src/pages/${controlSlug}/index.astro is missing, so there is no page to make a variant of.`);
52020
53071
  }
52021
- const internalId = await readInternalId(path39.join(controlDir, "_definition.md"));
53072
+ const internalId = await readInternalId(path41.join(controlDir, "_definition.md"));
52022
53073
  const variantSlugValue = variantSlug(
52023
53074
  controlSlug,
52024
53075
  nextVariantNumber(
@@ -52027,10 +53078,10 @@ async function resolveTargets(projectRoot, opts) {
52027
53078
  await archivedVariantNumbers(projectRoot, internalId)
52028
53079
  )
52029
53080
  );
52030
- const variantDir = path39.resolve(projectRoot, "src", "pages", variantSlugValue);
52031
- const available = await listComponents(path39.join(controlDir, "_components"));
53081
+ const variantDir = path41.resolve(projectRoot, "src", "pages", variantSlugValue);
53082
+ const available = await listComponents(path41.join(controlDir, "_components"));
52032
53083
  const forks = [];
52033
- for (const name of parseForks(opts.fork)) {
53084
+ for (const name of parseForks(opts.fork, opts.rawArgs)) {
52034
53085
  const resolved = resolveForkName(name, available);
52035
53086
  if (resolved === null) {
52036
53087
  fail7("NOT_FOUND", `"${name}" is not a component of ${controlSlug}.`, {
@@ -52044,26 +53095,26 @@ async function resolveTargets(projectRoot, opts) {
52044
53095
  }
52045
53096
  async function writeVariant(opts) {
52046
53097
  const { controlDir, variantDir, controlSlug, variantSlug: variantSlug2, forks } = opts;
52047
- const forkedTargets = new Set(forks.map((f) => path39.join(controlDir, "_components", f)));
53098
+ const forkedTargets = new Set(forks.map((f) => path41.join(controlDir, "_components", f)));
52048
53099
  const written = [];
52049
- const indexText = await readFile25(path39.join(controlDir, "index.astro"), "utf8");
53100
+ const indexText = await readFile27(path41.join(controlDir, "index.astro"), "utf8");
52050
53101
  await mkdir13(variantDir, { recursive: true });
52051
53102
  await writeFile18(
52052
- path39.join(variantDir, "index.astro"),
53103
+ path41.join(variantDir, "index.astro"),
52053
53104
  repointFile(indexText, { fromDir: controlDir, toDir: variantDir, controlDir, variantDir, forkedTargets }),
52054
53105
  "utf8"
52055
53106
  );
52056
53107
  written.push(`src/pages/${variantSlug2}/index.astro`);
52057
53108
  for (const fork of forks) {
52058
- const fromFile = path39.join(controlDir, "_components", fork);
52059
- const toFile = path39.join(variantDir, "_components", fork);
52060
- const text2 = await readFile25(fromFile, "utf8");
52061
- await mkdir13(path39.dirname(toFile), { recursive: true });
53109
+ const fromFile = path41.join(controlDir, "_components", fork);
53110
+ const toFile = path41.join(variantDir, "_components", fork);
53111
+ const text2 = await readFile27(fromFile, "utf8");
53112
+ await mkdir13(path41.dirname(toFile), { recursive: true });
52062
53113
  await writeFile18(
52063
53114
  toFile,
52064
53115
  repointFile(text2, {
52065
- fromDir: path39.dirname(fromFile),
52066
- toDir: path39.dirname(toFile),
53116
+ fromDir: path41.dirname(fromFile),
53117
+ toDir: path41.dirname(toFile),
52067
53118
  controlDir,
52068
53119
  variantDir,
52069
53120
  forkedTargets
@@ -52072,9 +53123,9 @@ async function writeVariant(opts) {
52072
53123
  );
52073
53124
  written.push(`src/pages/${variantSlug2}/_components/${fork}`);
52074
53125
  }
52075
- const controlDefinitionPath = path39.join(controlDir, "_definition.md");
53126
+ const controlDefinitionPath = path41.join(controlDir, "_definition.md");
52076
53127
  if (await exists(controlDefinitionPath)) {
52077
- const definition = buildVariantDefinition(await readFile25(controlDefinitionPath, "utf8"), {
53128
+ const definition = buildVariantDefinition(await readFile27(controlDefinitionPath, "utf8"), {
52078
53129
  internalId: randomUUID2().replace(/-/g, "").slice(0, 8),
52079
53130
  variantSlug: variantSlug2,
52080
53131
  controlSlug,
@@ -52082,11 +53133,11 @@ async function writeVariant(opts) {
52082
53133
  ...opts.because ? { because: opts.because } : {},
52083
53134
  ...opts.change ? { change: opts.change } : {}
52084
53135
  });
52085
- await writeFile18(path39.join(variantDir, "_definition.md"), definition, "utf8");
53136
+ await writeFile18(path41.join(variantDir, "_definition.md"), definition, "utf8");
52086
53137
  written.push(`src/pages/${variantSlug2}/_definition.md`);
52087
53138
  }
52088
- await mkdir13(path39.join(variantDir, "_images"), { recursive: true });
52089
- await writeFile18(path39.join(variantDir, "_images", ".gitkeep"), "", "utf8");
53139
+ await mkdir13(path41.join(variantDir, "_images"), { recursive: true });
53140
+ await writeFile18(path41.join(variantDir, "_images", ".gitkeep"), "", "utf8");
52090
53141
  return written;
52091
53142
  }
52092
53143
  var variantCommand = defineCommand174({
@@ -52111,13 +53162,28 @@ var variantCommand = defineCommand174({
52111
53162
  description: "What is different about this version, in the client's words \u2014 \u201Cput the booking form in the hero\u201D."
52112
53163
  }
52113
53164
  },
52114
- async run({ args }) {
53165
+ async run({ args, rawArgs }) {
52115
53166
  const projectRoot = process.cwd();
52116
53167
  const controlSlug = String(args.page);
52117
53168
  const { controlDir, variantDir, variantSlug: variantSlug2, available, forks } = await resolveTargets(projectRoot, {
52118
53169
  controlSlug,
52119
- fork: args.fork
53170
+ fork: args.fork,
53171
+ rawArgs
52120
53172
  });
53173
+ let status;
53174
+ try {
53175
+ const response = await apiPost("/api/experiments/status", {
53176
+ landingSlug: controlSlug
53177
+ });
53178
+ status = response.data;
53179
+ } catch (error) {
53180
+ fail7(
53181
+ "UNAVAILABLE",
53182
+ `Could not check whether ${controlSlug} is already in a test (${error instanceof Error ? error.message : String(error)}). Nothing was written \u2014 run \`baker experiment status --landing ${controlSlug}\` and try again.`
53183
+ );
53184
+ }
53185
+ const taken = pageInTest(status, controlSlug);
53186
+ if (taken) fail7("CONFLICT", taken, { landing: controlSlug });
52121
53187
  const written = await writeVariant({
52122
53188
  controlDir,
52123
53189
  variantDir,
@@ -54093,8 +55159,8 @@ var listCommand16 = defineCommand193({
54093
55159
  });
54094
55160
 
54095
55161
  // src/commands/scheduled-actions/templates.ts
54096
- import { readFile as readFile26 } from "fs/promises";
54097
- import path40 from "path";
55162
+ import { readFile as readFile28 } from "fs/promises";
55163
+ import path42 from "path";
54098
55164
  import { defineCommand as defineCommand194 } from "citty";
54099
55165
  registerSchema({
54100
55166
  command: "scheduled-actions.templates",
@@ -54197,7 +55263,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
54197
55263
  }
54198
55264
  if (save.length > 0) {
54199
55265
  const briefFile = flag("brief-file");
54200
- const brief = briefFile.length > 0 ? await readFile26(path40.resolve(briefFile), "utf8") : flag("brief");
55266
+ const brief = briefFile.length > 0 ? await readFile28(path42.resolve(briefFile), "utf8") : flag("brief");
54201
55267
  if (brief.trim().length === 0) {
54202
55268
  failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
54203
55269
  }
@@ -54318,7 +55384,7 @@ registerSchema({
54318
55384
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
54319
55385
  }
54320
55386
  });
54321
- var updateCommand3 = defineCommand196({
55387
+ var updateCommand4 = defineCommand196({
54322
55388
  meta: {
54323
55389
  name: "update",
54324
55390
  description: "Stage a scheduled action update. Examples: baker scheduled-actions update <id> --enabled false | baker scheduled-actions update <id> --mode publish"
@@ -54417,7 +55483,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
54417
55483
  list: listCommand16,
54418
55484
  get: getCommand4,
54419
55485
  create: createCommand3,
54420
- update: updateCommand3,
55486
+ update: updateCommand4,
54421
55487
  delete: deleteCommand3,
54422
55488
  templates: templatesCommand,
54423
55489
  trigger: triggerCommand
@@ -54668,7 +55734,7 @@ function parseImageRefs(spec) {
54668
55734
  }
54669
55735
  var defaultDeps = {
54670
55736
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
54671
- upload: (path42) => uploadLocalImage({ file: path42, contentType: detectImageContentType(path42), source: "uploaded" })
55737
+ upload: (path44) => uploadLocalImage({ file: path44, contentType: detectImageContentType(path44), source: "uploaded" })
54672
55738
  };
54673
55739
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
54674
55740
  const refs = parseImageRefs(spec);
@@ -54688,10 +55754,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
54688
55754
  }
54689
55755
  return { imageIds, added };
54690
55756
  }
54691
- function uploadFailure(path42) {
55757
+ function uploadFailure(path44) {
54692
55758
  return (error) => {
54693
55759
  if (error instanceof ApiError) throw error;
54694
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path42}" as an image.`);
55760
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path44}" as an image.`);
54695
55761
  };
54696
55762
  }
54697
55763
 
@@ -55689,7 +56755,7 @@ import { defineCommand as defineCommand211 } from "citty";
55689
56755
  import { defineCommand as defineCommand208 } from "citty";
55690
56756
 
55691
56757
  // src/commands/tag-manager/shared.ts
55692
- import { readFileSync as readFileSync17 } from "fs";
56758
+ import { readFileSync as readFileSync18 } from "fs";
55693
56759
  function failValidation5(message) {
55694
56760
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
55695
56761
  process.exit(1);
@@ -55705,7 +56771,7 @@ function requireTarget6(args, entity) {
55705
56771
  function loadJsonArg2(args, flag = "json") {
55706
56772
  const inline = args[flag];
55707
56773
  const file = args.file;
55708
- const raw = typeof file === "string" && file.length > 0 ? readFileSync17(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
56774
+ const raw = typeof file === "string" && file.length > 0 ? readFileSync18(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
55709
56775
  if (raw === void 0) {
55710
56776
  failValidation5(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
55711
56777
  }
@@ -55753,10 +56819,10 @@ async function stageOp4(op) {
55753
56819
  handleError5(err);
55754
56820
  }
55755
56821
  }
55756
- async function draftAction3(path42, body, chat) {
56822
+ async function draftAction3(path44, body, chat) {
55757
56823
  const chatId = resolveChatId(chat);
55758
56824
  try {
55759
- const data = await apiPost(path42, { chatId, ...body });
56825
+ const data = await apiPost(path44, { chatId, ...body });
55760
56826
  writeJsonEnvelope({ ok: true, data });
55761
56827
  return data;
55762
56828
  } catch (err) {
@@ -56891,9 +57957,9 @@ var groupCommand2 = defineCommand219({
56891
57957
  });
56892
57958
 
56893
57959
  // src/commands/videos/ingest.ts
56894
- import { mkdtemp as mkdtemp2, rm as rm8, stat as stat9 } from "fs/promises";
57960
+ import { mkdtemp as mkdtemp2, rm as rm8, stat as stat10 } from "fs/promises";
56895
57961
  import { tmpdir as tmpdir3 } from "os";
56896
- import path41 from "path";
57962
+ import path43 from "path";
56897
57963
  import { defineCommand as defineCommand220 } from "citty";
56898
57964
 
56899
57965
  // src/lib/streamUpload.ts
@@ -57244,7 +58310,7 @@ function ingestUrl(args) {
57244
58310
  }
57245
58311
  async function downloadThenIngest(args, country) {
57246
58312
  const vimeoCookie = captureVimeoCookie();
57247
- const workDir = await mkdtemp2(path41.join(tmpdir3(), "videos-ingest-"));
58313
+ const workDir = await mkdtemp2(path43.join(tmpdir3(), "videos-ingest-"));
57248
58314
  try {
57249
58315
  const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
57250
58316
  if (isAudioOnly(probe.info)) {
@@ -57275,7 +58341,7 @@ async function downloadThenIngest(args, country) {
57275
58341
  // we allow to fetch it cannot drift apart.
57276
58342
  timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
57277
58343
  });
57278
- const stats = await stat9(filePath);
58344
+ const stats = await stat10(filePath);
57279
58345
  if (stats.size > MAX_VIDEO_INGEST_BYTES) {
57280
58346
  throw new ApiError(
57281
58347
  "VALIDATION_ERROR",
@@ -57380,7 +58446,7 @@ var searchCommand4 = defineCommand221({
57380
58446
  var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
57381
58447
 
57382
58448
  // src/commands/videos/upload.ts
57383
- import { readFile as readFile27, stat as stat10 } from "fs/promises";
58449
+ import { readFile as readFile29, stat as stat11 } from "fs/promises";
57384
58450
  import { basename as basename3, extname as extname4 } from "path";
57385
58451
  import { defineCommand as defineCommand222 } from "citty";
57386
58452
  var MIME_MAP = {
@@ -57464,7 +58530,7 @@ var uploadCommand2 = defineCommand222({
57464
58530
  const originalFilename = basename3(filePath);
57465
58531
  const descriptionContext = args.context;
57466
58532
  if (args["dry-run"]) {
57467
- const fileStats = await stat10(filePath);
58533
+ const fileStats = await stat11(filePath);
57468
58534
  writeJson({
57469
58535
  ok: true,
57470
58536
  dryRun: true,
@@ -57477,7 +58543,7 @@ var uploadCommand2 = defineCommand222({
57477
58543
  originalFilename,
57478
58544
  descriptionContext
57479
58545
  });
57480
- const fileBuffer = await readFile27(filePath);
58546
+ const fileBuffer = await readFile29(filePath);
57481
58547
  const uploadResponse = await fetch(uploadUrl, {
57482
58548
  method: "PUT",
57483
58549
  headers: { "Content-Type": contentType },
@@ -58864,7 +59930,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
58864
59930
  };
58865
59931
  }
58866
59932
  function commandPathOf(root, argv) {
58867
- const path42 = [];
59933
+ const path44 = [];
58868
59934
  let command = root;
58869
59935
  for (const token of argv) {
58870
59936
  if (token === "--" || token.startsWith("-")) {
@@ -58875,10 +59941,10 @@ function commandPathOf(root, argv) {
58875
59941
  if (next === void 0 || typeof next !== "object") {
58876
59942
  break;
58877
59943
  }
58878
- path42.push(token);
59944
+ path44.push(token);
58879
59945
  command = next;
58880
59946
  }
58881
- return path42.join(" ");
59947
+ return path44.join(" ");
58882
59948
  }
58883
59949
  function refuseUnknownFlags(root, argv) {
58884
59950
  const unknown = findUnknownFlags(root, argv);
@@ -58896,7 +59962,7 @@ function refuseUnknownFlags(root, argv) {
58896
59962
  }
58897
59963
 
58898
59964
  // src/version.ts
58899
- import { readFileSync as readFileSync18 } from "fs";
59965
+ import { readFileSync as readFileSync19 } from "fs";
58900
59966
  function packageJsonUrl() {
58901
59967
  return new URL("../package.json", import.meta.url);
58902
59968
  }
@@ -58908,7 +59974,7 @@ function parsePackageVersion(raw) {
58908
59974
  throw new Error("Invalid CLI package.json: missing version");
58909
59975
  }
58910
59976
  function getCliVersion() {
58911
- return parsePackageVersion(readFileSync18(packageJsonUrl(), "utf8"));
59977
+ return parsePackageVersion(readFileSync19(packageJsonUrl(), "utf8"));
58912
59978
  }
58913
59979
 
58914
59980
  // src/cli.ts
@@ -58947,7 +60013,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
58947
60013
  tags: tagsCommand4,
58948
60014
  "tag-manager": tagManagerCommand,
58949
60015
  chats: chatsCommand,
58950
- history: historyCommand,
60016
+ history: historyCommand2,
58951
60017
  hubspot: hubspotCommand,
58952
60018
  "winning-ads": winningAdsCommand,
58953
60019
  mcp: mcpCommand,