@koda-sl/baker-cli 0.286.0-dev.93be96120 → 0.290.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -29
- package/dist/{chunk-LN5O53BG.js → chunk-V7BIRLPU.js} +4 -4
- package/dist/chunk-V7BIRLPU.js.map +1 -0
- package/dist/cli.js +1776 -687
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LN5O53BG.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
ulid,
|
|
59
59
|
validateCanvasDeep,
|
|
60
60
|
ytDlpBlockSignal
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-V7BIRLPU.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
|
|
10512
|
+
const walk2 = (node) => {
|
|
10513
10513
|
accounts.add(node.customerId);
|
|
10514
10514
|
for (const child of node.children) {
|
|
10515
|
-
|
|
10515
|
+
walk2(child);
|
|
10516
10516
|
}
|
|
10517
10517
|
};
|
|
10518
10518
|
for (const node of nodes) {
|
|
10519
|
-
|
|
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(
|
|
10687
|
-
if (typeof
|
|
10686
|
+
function rawFileEntries(path44) {
|
|
10687
|
+
if (typeof path44 !== "string" || path44.length === 0) {
|
|
10688
10688
|
return [];
|
|
10689
10689
|
}
|
|
10690
|
-
return readFileSync2(
|
|
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(
|
|
10714
|
-
if (typeof
|
|
10713
|
+
function loadJsonFileArg(path44) {
|
|
10714
|
+
if (typeof path44 !== "string" || path44.length === 0) {
|
|
10715
10715
|
return {};
|
|
10716
10716
|
}
|
|
10717
|
-
const inline =
|
|
10718
|
-
const source = inline ? "inline JSON" :
|
|
10717
|
+
const inline = path44.trimStart().startsWith("{");
|
|
10718
|
+
const source = inline ? "inline JSON" : path44;
|
|
10719
10719
|
try {
|
|
10720
|
-
const parsed = JSON.parse(inline ?
|
|
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(
|
|
10857
|
+
async function draftAction(path44, body, chat) {
|
|
10858
10858
|
try {
|
|
10859
10859
|
const chatId = resolveChatId(chat);
|
|
10860
|
-
const response = await apiPost(
|
|
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(
|
|
15231
|
-
const inline =
|
|
15276
|
+
function readRowsFile(path44) {
|
|
15277
|
+
const inline = path44.trimStart().startsWith("[") || path44.trimStart().startsWith("{");
|
|
15232
15278
|
let text2;
|
|
15233
15279
|
try {
|
|
15234
|
-
text2 = inline ?
|
|
15280
|
+
text2 = inline ? path44 : readFileSync4(path44, "utf8");
|
|
15235
15281
|
} catch (err) {
|
|
15236
|
-
failWriteValidation(`could not read ${
|
|
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" :
|
|
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(
|
|
17080
|
-
if (typeof
|
|
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(
|
|
17130
|
+
const parsed = JSON.parse(readFileSync5(path44, "utf8"));
|
|
17085
17131
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
17086
|
-
failWriteValidation2(`${
|
|
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(`${
|
|
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(
|
|
17177
|
-
if (typeof
|
|
17222
|
+
function loadTargetingFileArg(path44) {
|
|
17223
|
+
if (typeof path44 !== "string" || path44.length === 0) {
|
|
17178
17224
|
return void 0;
|
|
17179
17225
|
}
|
|
17180
|
-
const parsed = loadJsonFileArg2(
|
|
17226
|
+
const parsed = loadJsonFileArg2(path44);
|
|
17181
17227
|
const criteria = parsed.targetingCriteria ?? parsed;
|
|
17182
17228
|
if (!criteria.include) {
|
|
17183
17229
|
failWriteValidation2(
|
|
17184
|
-
`${
|
|
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(
|
|
17220
|
-
if (typeof
|
|
17265
|
+
function parseListFileArg(path44, maxRows) {
|
|
17266
|
+
if (typeof path44 !== "string" || path44.length === 0) {
|
|
17221
17267
|
return void 0;
|
|
17222
17268
|
}
|
|
17223
|
-
const raw = readFileSync5(
|
|
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(`${
|
|
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(`${
|
|
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(
|
|
17342
|
-
if (typeof
|
|
17387
|
+
function idsFileEntries(path44) {
|
|
17388
|
+
if (typeof path44 !== "string" || path44.length === 0) {
|
|
17343
17389
|
return [];
|
|
17344
17390
|
}
|
|
17345
|
-
return readFileSync5(
|
|
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,
|
|
20111
|
+
function readPath(row, path44) {
|
|
20066
20112
|
let current = row;
|
|
20067
|
-
for (const segment of
|
|
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
|
|
20078
|
-
const value = readPath(row,
|
|
20123
|
+
for (const path44 of paths) {
|
|
20124
|
+
const value = readPath(row, path44);
|
|
20079
20125
|
if (value !== void 0) {
|
|
20080
|
-
projected[
|
|
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
|
|
20381
|
+
const walk2 = (node) => {
|
|
20336
20382
|
accounts.add(node.accountId);
|
|
20337
20383
|
for (const child of node.children) {
|
|
20338
|
-
|
|
20384
|
+
walk2(child);
|
|
20339
20385
|
}
|
|
20340
20386
|
};
|
|
20341
20387
|
for (const node of nodes) {
|
|
20342
|
-
|
|
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,
|
|
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:
|
|
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(
|
|
22441
|
-
if (typeof
|
|
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(
|
|
22491
|
+
const parsed = JSON.parse(readFileSync9(path44, "utf8"));
|
|
22446
22492
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
22447
|
-
failWriteValidation3(`${
|
|
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(`${
|
|
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
|
|
25411
|
+
const walk2 = (node) => {
|
|
25366
25412
|
accounts.add(node.accountId);
|
|
25367
25413
|
for (const child of node.children) {
|
|
25368
|
-
|
|
25414
|
+
walk2(child);
|
|
25369
25415
|
}
|
|
25370
25416
|
};
|
|
25371
25417
|
for (const node of nodes) {
|
|
25372
|
-
|
|
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.
|
|
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
|
|
28015
|
-
const
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
const
|
|
28019
|
-
|
|
28020
|
-
|
|
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 &&
|
|
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 (
|
|
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
|
-
|
|
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
|
|
28034
|
-
|
|
28035
|
-
|
|
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
|
|
28327
|
+
return rows;
|
|
28038
28328
|
}
|
|
28039
|
-
function
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
28043
|
-
|
|
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
|
|
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'.
|
|
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
|
|
29011
|
-
|
|
29012
|
-
|
|
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
|
-
|
|
29019
|
-
|
|
29020
|
-
|
|
29021
|
-
|
|
29022
|
-
|
|
29023
|
-
|
|
29024
|
-
|
|
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(
|
|
29464
|
-
return unwrap(await apiGet(
|
|
30015
|
+
async function readAvatars(path44, params) {
|
|
30016
|
+
return unwrap(await apiGet(path44, params));
|
|
29465
30017
|
}
|
|
29466
|
-
async function writeAvatars(
|
|
29467
|
-
return unwrap(await apiPost(
|
|
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:
|
|
30302
|
-
const dir = posix.dirname(
|
|
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(
|
|
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
|
|
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 =
|
|
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:
|
|
35832
|
+
const { readdir: readdir14 } = await import("fs/promises");
|
|
35281
35833
|
let entries;
|
|
35282
35834
|
try {
|
|
35283
|
-
entries = await
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
37625
|
+
import { existsSync as existsSync5 } from "fs";
|
|
37074
37626
|
import path21 from "path";
|
|
37075
|
-
function resolveShippedCanvasDir(name, startDir, exists2 =
|
|
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
|
|
38572
|
-
import
|
|
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
|
|
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 +
|
|
39197
|
+
return abs === dir || abs.startsWith(dir + path28.sep);
|
|
38601
39198
|
}
|
|
38602
39199
|
function toSpecifier(from, to) {
|
|
38603
|
-
const rel =
|
|
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 =
|
|
39205
|
+
const abs = path28.resolve(opts.fromDir, spec);
|
|
38609
39206
|
if (opts.forkedTargets?.has(abs)) {
|
|
38610
|
-
const withinControl =
|
|
38611
|
-
return toSpecifier(opts.toDir,
|
|
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 =
|
|
38624
|
-
const within =
|
|
38625
|
-
const target = within.startsWith("..") ||
|
|
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(
|
|
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
|
|
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(
|
|
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 +
|
|
38661
|
-
return posix2(
|
|
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,
|
|
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 =
|
|
38670
|
-
await mkdir8(
|
|
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
|
|
39271
|
+
const text2 = await readFile22(abs, "utf8");
|
|
38675
39272
|
await writeFile11(dest, repointIntoArchive(text2, abs, dest, archivedBySource), "utf8");
|
|
38676
39273
|
}
|
|
38677
|
-
written.push(posix2(
|
|
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) =>
|
|
38685
|
-
for (const rel of await filesUnder(armDir)) reachable.add(
|
|
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(
|
|
39296
|
+
const resolved = resolveWithExtensions(path29.resolve(path29.dirname(sourceAbs), spec), archivedBySource);
|
|
38700
39297
|
if (resolved === null) return match;
|
|
38701
|
-
const rel = posix2(
|
|
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 =
|
|
38718
|
-
const to =
|
|
38719
|
-
await mkdir8(
|
|
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
|
|
39320
|
+
const text2 = await readFile22(from, "utf8");
|
|
38724
39321
|
await writeFile11(
|
|
38725
39322
|
to,
|
|
38726
39323
|
repointMoved(text2, {
|
|
38727
|
-
fromDir:
|
|
38728
|
-
toDir:
|
|
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(
|
|
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 =
|
|
38760
|
-
const variantDir =
|
|
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 =
|
|
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(
|
|
38785
|
-
written.push(posix2(
|
|
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,59 +39387,48 @@ async function foldExperiment(root, target) {
|
|
|
38790
39387
|
return {
|
|
38791
39388
|
experimentId: target.experimentId,
|
|
38792
39389
|
page: target.landingSlug,
|
|
38793
|
-
archivedTo: posix2(
|
|
39390
|
+
archivedTo: posix2(path29.join(ARCHIVE_ROOT, archiveKey, String(variantNumber))),
|
|
38794
39391
|
written,
|
|
38795
39392
|
removed: [`src/pages/${target.variantSlug}`]
|
|
38796
39393
|
};
|
|
38797
39394
|
}
|
|
38798
39395
|
|
|
38799
|
-
// src/commands/experiment/goal.ts
|
|
38800
|
-
function parseEventGoal(argument) {
|
|
38801
|
-
const [name, filter] = argument.split("/");
|
|
38802
|
-
if (!name) return { error: "Name the event: --goal event:request_demo" };
|
|
38803
|
-
if (!filter) return { kind: "custom_event", name };
|
|
38804
|
-
const separator = filter.indexOf("=");
|
|
38805
|
-
if (separator === -1) {
|
|
38806
|
-
return { error: `Write the property as key=value: --goal event:${name}/section=pricing` };
|
|
38807
|
-
}
|
|
38808
|
-
return { kind: "custom_event", name, property: filter.slice(0, separator), value: filter.slice(separator + 1) };
|
|
38809
|
-
}
|
|
38810
|
-
function parseGoal(raw) {
|
|
38811
|
-
if (raw === void 0 || raw === "") return void 0;
|
|
38812
|
-
const separator = raw.indexOf(":");
|
|
38813
|
-
const kind = separator === -1 ? raw : raw.slice(0, separator);
|
|
38814
|
-
const argument = separator === -1 ? "" : raw.slice(separator + 1);
|
|
38815
|
-
if (kind === "leads") return argument ? { kind: "conversion", flowSlug: argument } : { kind: "conversion" };
|
|
38816
|
-
if (kind === "click") return argument ? { kind: "outbound_click", targetHost: argument } : { kind: "outbound_click" };
|
|
38817
|
-
if (kind === "event") return parseEventGoal(argument);
|
|
38818
|
-
return {
|
|
38819
|
-
error: `Unknown goal "${raw}". Use leads, leads:<form>, event:<name>, event:<name>/<key>=<value>, click, or click:<host>`
|
|
38820
|
-
};
|
|
38821
|
-
}
|
|
38822
|
-
|
|
38823
39396
|
// src/commands/experiment/hints.ts
|
|
38824
|
-
var
|
|
39397
|
+
var LIVE_HINT = {
|
|
38825
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)}`,
|
|
38826
|
-
winner: (row) => `${row.experimentId} has a winner. Run \`baker experiment finish --id ${row.experimentId}\` to
|
|
38827
|
-
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.
|
|
38828
|
-
stopped_early_harmful: (row) => `${row.experimentId} is doing damage
|
|
38829
|
-
// Unreachable
|
|
38830
|
-
//
|
|
38831
|
-
//
|
|
38832
|
-
|
|
38833
|
-
invalid: (row) => `${row.experimentId} cannot be read: ${row.invalidReason === "crossover" ? "too many visitors saw both versions" : "the traffic did not split evenly"}.
|
|
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.`,
|
|
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.`,
|
|
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.`,
|
|
39402
|
+
// Unreachable on a live test — only ending one produces it. The Record is
|
|
39403
|
+
// exhaustive by type so a verdict added upstream fails to compile here rather
|
|
39404
|
+
// than printing nothing.
|
|
39405
|
+
inconclusive: (row) => `${row.experimentId} was ended before it concluded and found nothing.`,
|
|
39406
|
+
invalid: (row) => `${row.experimentId} cannot be read: ${row.invalidReason === "crossover" ? "too many visitors saw both versions" : "the traffic did not split evenly"}. End it with \`--keep original\` and start again rather than reporting anything from it.`
|
|
38834
39407
|
};
|
|
38835
39408
|
function describeWait(row) {
|
|
38836
39409
|
if (row.willNotConclude) {
|
|
38837
|
-
return "This page's traffic will not settle it at any point, so
|
|
39410
|
+
return "This page's traffic will not settle it at any point, so end it with `--keep original` and test a bigger change.";
|
|
38838
39411
|
}
|
|
38839
39412
|
if (row.earliestDecisionAt === null) {
|
|
38840
39413
|
return "This page has produced no visitors yet, so there is nothing to project a finish date from.";
|
|
38841
39414
|
}
|
|
38842
39415
|
return `Earliest it could say anything: ${new Date(row.earliestDecisionAt).toISOString().slice(0, 10)}.`;
|
|
38843
39416
|
}
|
|
39417
|
+
function pausedNote(row) {
|
|
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. ` : "";
|
|
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
|
+
}
|
|
38844
39428
|
function buildStatusHints(experiments) {
|
|
38845
|
-
const hints2 = experiments.filter((row) => row.status
|
|
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);
|
|
38846
39432
|
if (experiments.length === 0) {
|
|
38847
39433
|
hints2.push(
|
|
38848
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."
|
|
@@ -38853,40 +39439,122 @@ function buildStatusHints(experiments) {
|
|
|
38853
39439
|
function asLift(value) {
|
|
38854
39440
|
return `+${Math.round(value * 100)}%`;
|
|
38855
39441
|
}
|
|
38856
|
-
function
|
|
38857
|
-
|
|
38858
|
-
if (
|
|
38859
|
-
return
|
|
38860
|
-
}
|
|
38861
|
-
|
|
38862
|
-
}
|
|
38863
|
-
|
|
38864
|
-
|
|
39442
|
+
function horizonsHint(plan) {
|
|
39443
|
+
const settled = plan.horizons.filter((horizon) => horizon.detectableLift !== null);
|
|
39444
|
+
if (settled.length === 0) {
|
|
39445
|
+
return "At this page's traffic no window up to 90 days would gather enough visitors to settle anything, at any size of change. The constraint is the traffic, not the test.";
|
|
39446
|
+
}
|
|
39447
|
+
const parts = settled.map((horizon) => `${horizon.days} days \u2192 ${asLift(horizon.detectableLift ?? 0)}`);
|
|
39448
|
+
return `The smallest change each window could settle on this page's traffic: ${parts.join(", ")}. Design the alternative to be at least that different.`;
|
|
39449
|
+
}
|
|
39450
|
+
function conversionsHint(plan) {
|
|
39451
|
+
if (plan.conversions.length <= 1) return null;
|
|
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}`;
|
|
39473
|
+
}
|
|
39474
|
+
function buildPlanHints(plan) {
|
|
39475
|
+
const horizons = horizonsHint(plan);
|
|
39476
|
+
const past = pastTestsHint(plan);
|
|
38865
39477
|
if (!plan.canRun) {
|
|
38866
39478
|
return [
|
|
38867
39479
|
plan.reason ?? "This test cannot run on this page.",
|
|
38868
|
-
...
|
|
39480
|
+
...horizons && plan.visitors > 0 ? [horizons] : [],
|
|
39481
|
+
...past ? [past] : [],
|
|
38869
39482
|
"A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
|
|
38870
39483
|
];
|
|
38871
39484
|
}
|
|
38872
39485
|
const hints2 = [
|
|
38873
|
-
|
|
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] : [],
|
|
39491
|
+
`This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
|
|
38874
39492
|
// Fires at the exact moment the hypothesis gets invented, whether or not
|
|
38875
39493
|
// the agent read the family doc. `plan` says a question CAN be settled here
|
|
38876
39494
|
// and says nothing about which question is worth asking — and a well-run
|
|
38877
|
-
// test of a bad idea
|
|
38878
|
-
"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
|
|
39495
|
+
// test of a bad idea spends the page's traffic and returns `no_difference`.
|
|
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.",
|
|
38879
39497
|
// The base always works, so a missing integration is never a reason to
|
|
38880
39498
|
// stop. Naming what it would have shown is worth more to the client than
|
|
38881
39499
|
// either a silent gap or a run that waited.
|
|
38882
39500
|
"If none of that is connected, this page's own drop-off is enough to test on \u2014 say which source you lacked and what it would have told you, then run the test anyway. A test never waits on an integration."
|
|
38883
39501
|
];
|
|
39502
|
+
const conversions = conversionsHint(plan);
|
|
39503
|
+
if (conversions) hints2.push(conversions);
|
|
38884
39504
|
if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
|
|
38885
39505
|
hints2.push(
|
|
38886
|
-
`At this page's traffic it would take about ${plan.estimatedDays} days.
|
|
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.`
|
|
39507
|
+
);
|
|
39508
|
+
}
|
|
39509
|
+
if (horizons) hints2.push(horizons);
|
|
39510
|
+
return hints2;
|
|
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).`
|
|
38887
39556
|
);
|
|
38888
39557
|
}
|
|
38889
|
-
if (window2) hints2.push(window2);
|
|
38890
39558
|
return hints2;
|
|
38891
39559
|
}
|
|
38892
39560
|
|
|
@@ -38901,45 +39569,8 @@ var experimentCompositionSchema = z35.object({
|
|
|
38901
39569
|
|
|
38902
39570
|
// ../api/src/experiments/goal.ts
|
|
38903
39571
|
import { z as z36 } from "zod";
|
|
38904
|
-
var
|
|
38905
|
-
var experimentGoalSchema = z36.
|
|
38906
|
-
z36.object({
|
|
38907
|
-
kind: z36.literal("conversion"),
|
|
38908
|
-
/**
|
|
38909
|
-
* Narrow to one Form, by the slug that is its identity.
|
|
38910
|
-
*
|
|
38911
|
-
* Absent means every marked conversion on the page counts, which is the
|
|
38912
|
-
* right default for a landing with one Form and the wrong one for a page
|
|
38913
|
-
* carrying both a newsletter signup and a demo request.
|
|
38914
|
-
*/
|
|
38915
|
-
flowSlug: z36.string().max(120).optional()
|
|
38916
|
-
}),
|
|
38917
|
-
z36.object({
|
|
38918
|
-
kind: z36.literal("custom_event"),
|
|
38919
|
-
name: eventNameSchema,
|
|
38920
|
-
/**
|
|
38921
|
-
* Narrow to one value of one of the event's own properties.
|
|
38922
|
-
*
|
|
38923
|
-
* `section_view` on its own is every section; `section_view` where
|
|
38924
|
-
* `section = pricing` is the one that matters. Without this a page would
|
|
38925
|
-
* have to encode the answer into the event name to be testable against it,
|
|
38926
|
-
* which is precisely what the properties map exists to stop.
|
|
38927
|
-
*/
|
|
38928
|
-
property: z36.string().max(64).optional(),
|
|
38929
|
-
value: z36.string().max(255).optional()
|
|
38930
|
-
}),
|
|
38931
|
-
z36.object({
|
|
38932
|
-
kind: z36.literal("outbound_click"),
|
|
38933
|
-
/**
|
|
38934
|
-
* The destination host, as the row records it. Absent means any exit.
|
|
38935
|
-
*
|
|
38936
|
-
* A host rather than a full URL because that is what `event_name` carries
|
|
38937
|
-
* on an `outbound_click` row — `target_url` has the rest, and matching on
|
|
38938
|
-
* it would make the goal depend on query parameters a campaign rewrites.
|
|
38939
|
-
*/
|
|
38940
|
-
targetHost: z36.string().max(255).optional()
|
|
38941
|
-
})
|
|
38942
|
-
]);
|
|
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 });
|
|
38943
39574
|
|
|
38944
39575
|
// ../api/src/experiments/hypothesis.ts
|
|
38945
39576
|
import { z as z37 } from "zod";
|
|
@@ -38994,10 +39625,31 @@ var experimentHypothesisOutcomeSchema = z37.enum([
|
|
|
38994
39625
|
// ../api/src/experiments/wire.ts
|
|
38995
39626
|
import { z as z38 } from "zod";
|
|
38996
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");
|
|
38997
39632
|
var experimentPlanRequestSchema = z38.object({
|
|
38998
39633
|
landingSlug: slugSchema,
|
|
38999
|
-
variantSlug:
|
|
39634
|
+
variantSlug: variantSlugSchema,
|
|
39635
|
+
/**
|
|
39636
|
+
* Which of the company's conversions the test is about.
|
|
39637
|
+
*
|
|
39638
|
+
* Optional only when the company counts exactly one outcome, in which case
|
|
39639
|
+
* it is that one. A company counting several has to say which — a test that
|
|
39640
|
+
* "measures conversions" on a page with a newsletter box and a demo request
|
|
39641
|
+
* is measuring two things and deciding on their sum.
|
|
39642
|
+
*/
|
|
39000
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(),
|
|
39001
39653
|
/**
|
|
39002
39654
|
* The smallest lift worth detecting, relative to the baseline. `0.2` is +20%.
|
|
39003
39655
|
*
|
|
@@ -39008,19 +39660,38 @@ var experimentPlanRequestSchema = z38.object({
|
|
|
39008
39660
|
*/
|
|
39009
39661
|
minDetectableRelativeLift: z38.number().gt(0).max(10).optional(),
|
|
39010
39662
|
/**
|
|
39011
|
-
*
|
|
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.
|
|
39012
39665
|
*
|
|
39013
|
-
*
|
|
39014
|
-
* the
|
|
39015
|
-
*
|
|
39016
|
-
*
|
|
39017
|
-
*
|
|
39018
|
-
* settle, which is a change somebody can actually go and make.
|
|
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.
|
|
39019
39671
|
*/
|
|
39020
|
-
|
|
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()
|
|
39021
39680
|
});
|
|
39022
39681
|
var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
|
|
39023
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(),
|
|
39024
39695
|
/**
|
|
39025
39696
|
* What the two arms are made of, read off the import graph by the caller at
|
|
39026
39697
|
* the moment the test starts.
|
|
@@ -39032,34 +39703,61 @@ var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
|
|
|
39032
39703
|
*/
|
|
39033
39704
|
composition: experimentCompositionSchema.optional()
|
|
39034
39705
|
});
|
|
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);
|
|
39035
39709
|
var experimentFinishRequestSchema = z38.object({
|
|
39036
39710
|
experimentId: z38.string().min(1).max(64),
|
|
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()
|
|
39715
|
+
});
|
|
39716
|
+
var experimentPauseRequestSchema = z38.object({
|
|
39717
|
+
experimentId: z38.string().min(1).max(64)
|
|
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,
|
|
39037
39724
|
/**
|
|
39038
|
-
*
|
|
39039
|
-
*
|
|
39040
|
-
*
|
|
39041
|
-
*
|
|
39042
|
-
* `survivor` they get to pick: an abandoned test has no winner, keeps the
|
|
39043
|
-
* control, and is recorded as `abandoned` so it can never be reported later
|
|
39044
|
-
* as a result. Without the separate flag, "stop this" and "the variant won"
|
|
39045
|
-
* would be the same call.
|
|
39046
|
-
*
|
|
39047
|
-
* **It overrides the verdict, and does not defer to it.** The only outcome it
|
|
39048
|
-
* can actually disagree with is a variant win — every other one keeps the
|
|
39049
|
-
* control anyway — so reading it as "apply only when the numbers have nothing
|
|
39050
|
-
* to say" meant that abandoning a won test handed all the traffic to the
|
|
39051
|
-
* variant instead of keeping the original: the opposite of what this field
|
|
39052
|
-
* promises, on live visitors, from a flag whose whole purpose is to be the
|
|
39053
|
-
* conservative choice. The response reports `discardedVerdict` when it did
|
|
39054
|
-
* override one, because throwing away a real finding should be said out loud.
|
|
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.
|
|
39055
39729
|
*/
|
|
39056
|
-
|
|
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()
|
|
39057
39744
|
});
|
|
39058
39745
|
var experimentFoldRequestSchema = z38.object({
|
|
39059
39746
|
/** Which test to fold. Omitted ⇒ every fold this company owes. */
|
|
39060
39747
|
experimentId: z38.string().max(64).optional(),
|
|
39061
39748
|
/** The files are written. Baker drops the promotion once the build lands. */
|
|
39062
|
-
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()
|
|
39063
39761
|
});
|
|
39064
39762
|
var experimentFoldTargetSchema = z38.object({
|
|
39065
39763
|
experimentId: z38.string(),
|
|
@@ -39079,12 +39777,39 @@ var experimentFoldResponseSchema = z38.object({
|
|
|
39079
39777
|
});
|
|
39080
39778
|
var experimentStatusRequestSchema = z38.object({
|
|
39081
39779
|
experimentId: z38.string().max(64).optional(),
|
|
39780
|
+
/** Only the tests on one page, by its slug. */
|
|
39781
|
+
landingSlug: z38.string().max(200).optional(),
|
|
39082
39782
|
full: z38.boolean().optional()
|
|
39083
39783
|
});
|
|
39784
|
+
var experimentHistoryRequestSchema = z38.object({
|
|
39785
|
+
landingSlug: z38.string().max(200).optional(),
|
|
39786
|
+
limit: z38.number().int().positive().max(100).optional()
|
|
39787
|
+
});
|
|
39788
|
+
var experimentPageConversionSchema = z38.object({
|
|
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(),
|
|
39793
|
+
/** Distinct visitors to the page who went on to do this. */
|
|
39794
|
+
convertingVisitors: z38.number()
|
|
39795
|
+
});
|
|
39796
|
+
var experimentHorizonSchema = z38.object({
|
|
39797
|
+
days: z38.number(),
|
|
39798
|
+
/** The smallest relative lift detectable in that many days. `null` when none would be. */
|
|
39799
|
+
detectableLift: z38.number().nullable()
|
|
39800
|
+
});
|
|
39084
39801
|
var experimentPlanResponseSchema = z38.object({
|
|
39085
39802
|
canRun: z38.boolean(),
|
|
39086
39803
|
/** Present when `canRun` is false. Product language, ready to show. */
|
|
39087
39804
|
reason: z38.string().optional(),
|
|
39805
|
+
/** The event the test would be read on. Absent when none could be chosen. */
|
|
39806
|
+
goal: experimentGoalSchema.optional(),
|
|
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. */
|
|
39810
|
+
conversions: z38.array(experimentPageConversionSchema),
|
|
39811
|
+
/** Distinct visitors the page had over the sizing window. */
|
|
39812
|
+
visitors: z38.number(),
|
|
39088
39813
|
baselineRate: z38.number(),
|
|
39089
39814
|
minDetectableRelativeLift: z38.number(),
|
|
39090
39815
|
/** `null` when no amount of traffic could settle the hypothesis. */
|
|
@@ -39093,14 +39818,24 @@ var experimentPlanResponseSchema = z38.object({
|
|
|
39093
39818
|
dailyVisitorsPerVariant: z38.number(),
|
|
39094
39819
|
/** `null` when the page has no traffic to project from. */
|
|
39095
39820
|
estimatedDays: z38.number().nullable(),
|
|
39096
|
-
|
|
39097
|
-
|
|
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(),
|
|
39098
39831
|
/**
|
|
39099
|
-
*
|
|
39100
|
-
*
|
|
39101
|
-
*
|
|
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.
|
|
39102
39837
|
*/
|
|
39103
|
-
|
|
39838
|
+
pastTests: z38.array(z38.lazy(() => experimentHistoryEntrySchema)).default([])
|
|
39104
39839
|
});
|
|
39105
39840
|
var experimentVerdictSchema = z38.enum([
|
|
39106
39841
|
"keep_running",
|
|
@@ -39109,16 +39844,17 @@ var experimentVerdictSchema = z38.enum([
|
|
|
39109
39844
|
"invalid",
|
|
39110
39845
|
"stopped_early_harmful",
|
|
39111
39846
|
/**
|
|
39112
|
-
*
|
|
39113
|
-
* rule — only by `
|
|
39847
|
+
* Ended before the numbers could say anything. Never produced by the
|
|
39848
|
+
* stopping rule — only by ending a `keep_running` test.
|
|
39114
39849
|
*
|
|
39115
39850
|
* Its own verdict rather than folded into `no_difference`, because they are
|
|
39116
39851
|
* opposite claims: one says the two versions were measured and found alike,
|
|
39117
39852
|
* the other says nobody ever found out. Collapsing them would manufacture the
|
|
39118
39853
|
* exact finding this feature exists to refuse.
|
|
39119
39854
|
*/
|
|
39120
|
-
"
|
|
39855
|
+
"inconclusive"
|
|
39121
39856
|
]);
|
|
39857
|
+
var experimentStatusValueSchema = z38.enum(["running", "paused", "finished"]);
|
|
39122
39858
|
var experimentArmSchema = z38.object({
|
|
39123
39859
|
slug: z38.string(),
|
|
39124
39860
|
visitors: z38.number(),
|
|
@@ -39132,7 +39868,28 @@ var experimentArmSchema = z38.object({
|
|
|
39132
39868
|
* screens cannot show a client two different "this is your offer page".
|
|
39133
39869
|
* `null` before the first capture, and on a page whose capture failed.
|
|
39134
39870
|
*/
|
|
39135
|
-
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()
|
|
39883
|
+
});
|
|
39884
|
+
var experimentMetricSchema = z38.object({
|
|
39885
|
+
name: z38.string(),
|
|
39886
|
+
control: z38.object({ conversions: z38.number(), rate: z38.number() }),
|
|
39887
|
+
variant: z38.object({ conversions: z38.number(), rate: z38.number() }),
|
|
39888
|
+
relativeLift: z38.number(),
|
|
39889
|
+
/** The anytime-valid interval on the relative lift. */
|
|
39890
|
+
liftInterval: z38.tuple([z38.number(), z38.number()]),
|
|
39891
|
+
/** `better` when the interval sits above zero, `worse` below it, `unclear` across it. */
|
|
39892
|
+
reading: z38.enum(["better", "worse", "unclear"])
|
|
39136
39893
|
});
|
|
39137
39894
|
var experimentTimelineArmSchema = z38.object({
|
|
39138
39895
|
visitors: z38.number(),
|
|
@@ -39173,19 +39930,51 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39173
39930
|
* is not a hypothesis, "leads should go up by at least 20%" is.
|
|
39174
39931
|
*/
|
|
39175
39932
|
minDetectableRelativeLift: z38.number(),
|
|
39933
|
+
/** The event the verdict is read from on the page, by key. */
|
|
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. */
|
|
39176
39938
|
goalLabel: z38.string(),
|
|
39177
|
-
status:
|
|
39939
|
+
status: experimentStatusValueSchema,
|
|
39178
39940
|
startedAt: z38.number(),
|
|
39941
|
+
/** Set while paused. The split is off and everyone sees the original. */
|
|
39942
|
+
pausedAt: z38.number().nullable(),
|
|
39179
39943
|
finishedAt: z38.number().nullable(),
|
|
39180
39944
|
verdict: experimentVerdictSchema,
|
|
39181
|
-
/** Set only on `winner`. Which
|
|
39945
|
+
/** Set only on `winner`. Which version the numbers favour. */
|
|
39182
39946
|
winner: z38.enum(["control", "variant"]).nullable(),
|
|
39947
|
+
/**
|
|
39948
|
+
* Which version is live, once the test has ended. Chosen by whoever ended it,
|
|
39949
|
+
* and allowed to disagree with `winner` — the card says so when it does.
|
|
39950
|
+
*/
|
|
39951
|
+
survivor: z38.enum(["control", "variant"]).nullable(),
|
|
39183
39952
|
/** Set only on `invalid`. Why the numbers cannot be read. */
|
|
39184
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(),
|
|
39185
39968
|
/** What a person should be told, in one sentence, in product language. */
|
|
39186
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(),
|
|
39187
39974
|
control: experimentArmSchema,
|
|
39188
39975
|
variant: experimentArmSchema,
|
|
39976
|
+
/** Every other conversion the company counts, on the same two arms. */
|
|
39977
|
+
secondary: z38.array(experimentMetricSchema),
|
|
39189
39978
|
/**
|
|
39190
39979
|
* How close this test is to being able to say anything at all, 0 to 1.
|
|
39191
39980
|
*
|
|
@@ -39199,8 +39988,8 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39199
39988
|
earliestDecisionAt: z38.number().nullable(),
|
|
39200
39989
|
/**
|
|
39201
39990
|
* True when no amount of this page's traffic would settle the comparison.
|
|
39202
|
-
* A different thing from a page that is merely slow, and the reason to
|
|
39203
|
-
*
|
|
39991
|
+
* A different thing from a page that is merely slow, and the reason to end
|
|
39992
|
+
* a test rather than leave it running.
|
|
39204
39993
|
*/
|
|
39205
39994
|
willNotConclude: z38.boolean(),
|
|
39206
39995
|
/**
|
|
@@ -39216,6 +40005,13 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39216
40005
|
evidence: z38.object({
|
|
39217
40006
|
relativeLift: z38.number(),
|
|
39218
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(),
|
|
39219
40015
|
controlInterval: z38.tuple([z38.number(), z38.number()]),
|
|
39220
40016
|
variantInterval: z38.tuple([z38.number(), z38.number()]),
|
|
39221
40017
|
/** The anytime-valid interval on the relative lift — what the verdict is read from. */
|
|
@@ -39225,6 +40021,54 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39225
40021
|
var experimentStatusResponseSchema = z38.object({
|
|
39226
40022
|
experiments: z38.array(experimentStatusRowSchema)
|
|
39227
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
|
+
});
|
|
40065
|
+
var experimentFinishResponseSchema = z38.object({
|
|
40066
|
+
verdict: experimentVerdictSchema,
|
|
40067
|
+
survivor: z38.enum(["control", "variant"]),
|
|
40068
|
+
/** True when `keep` went against a verdict the numbers had reached. */
|
|
40069
|
+
againstVerdict: z38.boolean(),
|
|
40070
|
+
summary: z38.string()
|
|
40071
|
+
});
|
|
39228
40072
|
|
|
39229
40073
|
// src/commands/experiment/hypothesis.ts
|
|
39230
40074
|
var SOURCES = experimentEvidenceSourceSchema.options.join(", ");
|
|
@@ -39246,9 +40090,8 @@ function parseOne(raw) {
|
|
|
39246
40090
|
}
|
|
39247
40091
|
return href ? { source: source.data, note, href } : { source: source.data, note };
|
|
39248
40092
|
}
|
|
39249
|
-
function parseEvidence(raw) {
|
|
39250
|
-
|
|
39251
|
-
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 !== "");
|
|
39252
40095
|
if (entries.length === 0) return void 0;
|
|
39253
40096
|
const parsed = [];
|
|
39254
40097
|
for (const entry of entries) {
|
|
@@ -39259,20 +40102,70 @@ function parseEvidence(raw) {
|
|
|
39259
40102
|
return parsed;
|
|
39260
40103
|
}
|
|
39261
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
|
+
|
|
39262
40155
|
// src/commands/experiment/index.ts
|
|
39263
40156
|
var GOAL_ARG = {
|
|
39264
40157
|
type: "string",
|
|
39265
|
-
description: "
|
|
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.",
|
|
39266
40159
|
required: false
|
|
39267
40160
|
};
|
|
39268
|
-
var
|
|
40161
|
+
var VARIANT_GOAL_ARG = {
|
|
39269
40162
|
type: "string",
|
|
39270
|
-
description: "The
|
|
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.",
|
|
39271
40164
|
required: false
|
|
39272
40165
|
};
|
|
39273
|
-
var
|
|
40166
|
+
var LIFT_ARG = {
|
|
39274
40167
|
type: "string",
|
|
39275
|
-
description: "
|
|
40168
|
+
description: "The smallest improvement worth detecting, relative \u2014 0.2 is +20% (default). The single most consequential number here: halving it roughly quadruples the traffic needed. Ask for the smallest lift that would actually change what the client does, not the smallest one you would like to see.",
|
|
39276
40169
|
required: false
|
|
39277
40170
|
};
|
|
39278
40171
|
var BECAUSE_ARG = {
|
|
@@ -39295,6 +40188,30 @@ var EVIDENCE_ARG = {
|
|
|
39295
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.",
|
|
39296
40189
|
required: false
|
|
39297
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
|
+
};
|
|
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
|
+
}
|
|
39298
40215
|
function fail5(message) {
|
|
39299
40216
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
39300
40217
|
process.exit(1);
|
|
@@ -39308,43 +40225,40 @@ function reportApiError(error) {
|
|
|
39308
40225
|
}
|
|
39309
40226
|
registerSchema({
|
|
39310
40227
|
command: "experiment.plan",
|
|
39311
|
-
description: "Whether a page
|
|
40228
|
+
description: "Whether a page can settle a question, and how long it might take, before anything is built",
|
|
39312
40229
|
args: {
|
|
39313
40230
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39314
|
-
variant: { type: "string", description: "The
|
|
40231
|
+
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39315
40232
|
goal: GOAL_ARG,
|
|
40233
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
39316
40234
|
lift: LIFT_ARG,
|
|
39317
|
-
|
|
40235
|
+
baseline: BASELINE_ARG
|
|
39318
40236
|
}
|
|
39319
40237
|
});
|
|
39320
40238
|
var planCommand = defineCommand119({
|
|
39321
40239
|
meta: {
|
|
39322
40240
|
name: "plan",
|
|
39323
|
-
description: "Start here, BEFORE building the alternative page. Says
|
|
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."
|
|
39324
40242
|
},
|
|
39325
40243
|
args: {
|
|
39326
40244
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39327
|
-
variant: { type: "string", description: "The
|
|
40245
|
+
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39328
40246
|
goal: GOAL_ARG,
|
|
40247
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
39329
40248
|
lift: LIFT_ARG,
|
|
39330
|
-
|
|
40249
|
+
baseline: BASELINE_ARG
|
|
39331
40250
|
},
|
|
39332
40251
|
run: async ({ args }) => {
|
|
39333
|
-
const goal = parseGoal(args.goal ? String(args.goal) : void 0);
|
|
39334
|
-
if (goal && "error" in goal) fail5(goal.error);
|
|
39335
40252
|
try {
|
|
39336
40253
|
const response = await apiPost("/api/experiments/plan", {
|
|
39337
40254
|
landingSlug: String(args.landing),
|
|
39338
40255
|
variantSlug: String(args.variant),
|
|
39339
|
-
...goal ? { goal } : {},
|
|
40256
|
+
...args.goal ? { goal: String(args.goal) } : {},
|
|
40257
|
+
...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
|
|
39340
40258
|
...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
|
|
39341
|
-
...args.
|
|
39342
|
-
});
|
|
39343
|
-
writeJsonEnvelope({
|
|
39344
|
-
ok: true,
|
|
39345
|
-
data: response.data,
|
|
39346
|
-
hints: buildPlanHints(response.data, args.days ? Number(args.days) : void 0)
|
|
40259
|
+
...args.baseline ? { baselineRate: Number(args.baseline) } : {}
|
|
39347
40260
|
});
|
|
40261
|
+
writeJsonEnvelope({ ok: true, data: response.data, hints: buildPlanHints(response.data) });
|
|
39348
40262
|
} catch (error) {
|
|
39349
40263
|
reportApiError(error);
|
|
39350
40264
|
}
|
|
@@ -39352,47 +40266,60 @@ var planCommand = defineCommand119({
|
|
|
39352
40266
|
});
|
|
39353
40267
|
registerSchema({
|
|
39354
40268
|
command: "experiment.start",
|
|
39355
|
-
description: "Stage an A/B test between a page and
|
|
40269
|
+
description: "Stage an A/B test between a page and a new variant of it",
|
|
39356
40270
|
args: {
|
|
39357
40271
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39358
|
-
variant: { type: "string", description: "The
|
|
40272
|
+
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39359
40273
|
because: BECAUSE_ARG,
|
|
39360
40274
|
change: CHANGE_ARG,
|
|
39361
40275
|
expect: EXPECT_ARG,
|
|
39362
40276
|
evidence: EVIDENCE_ARG,
|
|
39363
40277
|
goal: GOAL_ARG,
|
|
39364
|
-
|
|
40278
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40279
|
+
lift: LIFT_ARG,
|
|
40280
|
+
baseline: BASELINE_ARG
|
|
39365
40281
|
}
|
|
39366
40282
|
});
|
|
39367
40283
|
var startCommand = defineCommand119({
|
|
39368
40284
|
meta: {
|
|
39369
40285
|
name: "start",
|
|
39370
|
-
description: "Stage a test between the page and its
|
|
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."
|
|
39371
40287
|
},
|
|
39372
40288
|
args: {
|
|
39373
40289
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39374
|
-
variant: { type: "string", description: "The
|
|
40290
|
+
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39375
40291
|
because: BECAUSE_ARG,
|
|
39376
40292
|
change: CHANGE_ARG,
|
|
39377
40293
|
expect: EXPECT_ARG,
|
|
39378
40294
|
evidence: EVIDENCE_ARG,
|
|
39379
40295
|
goal: GOAL_ARG,
|
|
39380
|
-
|
|
40296
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40297
|
+
lift: LIFT_ARG,
|
|
40298
|
+
baseline: BASELINE_ARG
|
|
39381
40299
|
},
|
|
39382
|
-
run: async ({ args }) => {
|
|
39383
|
-
const goal = parseGoal(args.goal ? String(args.goal) : void 0);
|
|
39384
|
-
if (goal && "error" in goal) fail5(goal.error);
|
|
40300
|
+
run: async ({ args, rawArgs }) => {
|
|
39385
40301
|
const expect = args.expect === void 0 ? "increase" : String(args.expect);
|
|
39386
40302
|
if (expect !== "increase" && expect !== "decrease") {
|
|
39387
40303
|
fail5("`--expect` is either `increase` or `decrease` \u2014 which way should the goal move if you are right?");
|
|
39388
40304
|
}
|
|
39389
|
-
const evidence = parseEvidence(args.evidence);
|
|
40305
|
+
const evidence = parseEvidence(args.evidence, rawArgs);
|
|
39390
40306
|
if (evidence && "error" in evidence) fail5(evidence.error);
|
|
39391
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
|
+
]);
|
|
39392
40317
|
try {
|
|
39393
40318
|
const response = await apiPost("/api/experiments/start", {
|
|
39394
40319
|
landingSlug: String(args.landing),
|
|
39395
40320
|
variantSlug: String(args.variant),
|
|
40321
|
+
...landingInternalId ? { landingInternalId } : {},
|
|
40322
|
+
...variantInternalId ? { variantInternalId } : {},
|
|
39396
40323
|
hypothesis: {
|
|
39397
40324
|
because: String(args.because),
|
|
39398
40325
|
change: String(args.change),
|
|
@@ -39400,20 +40327,25 @@ var startCommand = defineCommand119({
|
|
|
39400
40327
|
...evidence ? { evidence } : {}
|
|
39401
40328
|
},
|
|
39402
40329
|
...composition ? { composition } : {},
|
|
39403
|
-
...goal ? { goal } : {},
|
|
39404
|
-
...args.
|
|
40330
|
+
...args.goal ? { goal: String(args.goal) } : {},
|
|
40331
|
+
...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
|
|
40332
|
+
...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
|
|
40333
|
+
...args.baseline ? { baselineRate: Number(args.baseline) } : {}
|
|
39405
40334
|
});
|
|
39406
40335
|
writeJsonEnvelope({
|
|
39407
40336
|
ok: true,
|
|
39408
40337
|
data: response.data,
|
|
39409
40338
|
hints: [
|
|
39410
|
-
|
|
39411
|
-
|
|
39412
|
-
|
|
39413
|
-
|
|
39414
|
-
|
|
39415
|
-
|
|
39416
|
-
|
|
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
|
+
}),
|
|
40348
|
+
"Staged. The split starts when this session is published \u2014 publishing is the review of the new variant.",
|
|
39417
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."
|
|
39418
40350
|
]
|
|
39419
40351
|
});
|
|
@@ -39424,25 +40356,28 @@ var startCommand = defineCommand119({
|
|
|
39424
40356
|
});
|
|
39425
40357
|
registerSchema({
|
|
39426
40358
|
command: "experiment.status",
|
|
39427
|
-
description: "The verdict on every A/B test, running and finished",
|
|
40359
|
+
description: "The verdict on every A/B test, running, paused and finished",
|
|
39428
40360
|
args: {
|
|
39429
40361
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
40362
|
+
landing: LANDING_FILTER_ARG,
|
|
39430
40363
|
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
39431
40364
|
}
|
|
39432
40365
|
});
|
|
39433
40366
|
var statusCommand6 = defineCommand119({
|
|
39434
40367
|
meta: {
|
|
39435
40368
|
name: "status",
|
|
39436
|
-
description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | invalid | stopped_early_harmful. `summary` is one sentence you can show the client. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
|
|
40369
|
+
description: "The verdict on each test. Read `verdict` and nothing else to decide: keep_running | winner | no_difference | invalid | stopped_early_harmful. `summary` is one sentence you can show the client, and `secondary` lists every other conversion the company counts on the same two arms \u2014 context, never a second decision. The numbers under `--full` are for printing, never for deciding \u2014 a test that says keep_running has not finished, however good its numbers look."
|
|
39437
40370
|
},
|
|
39438
40371
|
args: {
|
|
39439
40372
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
40373
|
+
landing: LANDING_FILTER_ARG,
|
|
39440
40374
|
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
39441
40375
|
},
|
|
39442
40376
|
run: async ({ args }) => {
|
|
39443
40377
|
try {
|
|
39444
40378
|
const response = await apiPost("/api/experiments/status", {
|
|
39445
40379
|
...args.id ? { experimentId: String(args.id) } : {},
|
|
40380
|
+
...args.landing ? { landingSlug: String(args.landing) } : {},
|
|
39446
40381
|
...args.full === true ? { full: true } : {}
|
|
39447
40382
|
});
|
|
39448
40383
|
writeJsonEnvelope({
|
|
@@ -39456,45 +40391,218 @@ var statusCommand6 = defineCommand119({
|
|
|
39456
40391
|
}
|
|
39457
40392
|
});
|
|
39458
40393
|
registerSchema({
|
|
39459
|
-
command: "experiment.
|
|
39460
|
-
description: "
|
|
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",
|
|
39461
40396
|
args: {
|
|
39462
|
-
|
|
39463
|
-
|
|
39464
|
-
|
|
39465
|
-
|
|
39466
|
-
|
|
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);
|
|
39467
40425
|
}
|
|
39468
40426
|
}
|
|
39469
40427
|
});
|
|
40428
|
+
registerSchema({
|
|
40429
|
+
command: "experiment.finish",
|
|
40430
|
+
description: "End a test now and send all its traffic to the version you name",
|
|
40431
|
+
args: {
|
|
40432
|
+
id: ID_ARG,
|
|
40433
|
+
keep: {
|
|
40434
|
+
type: "string",
|
|
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.",
|
|
40436
|
+
required: true
|
|
40437
|
+
},
|
|
40438
|
+
reason: REASON_ARG,
|
|
40439
|
+
learning: LEARNING_ARG
|
|
40440
|
+
}
|
|
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
|
+
}
|
|
39470
40465
|
var finishCommand = defineCommand119({
|
|
39471
40466
|
meta: {
|
|
39472
40467
|
name: "finish",
|
|
39473
|
-
description: "End a test and send all its traffic to
|
|
40468
|
+
description: "End a test now and send all its traffic to the version you name. Takes effect within seconds \u2014 nothing to publish. Read `baker experiment status` first: `winner` says which version the numbers favour, `keep_running` means they have not spoken yet and ending records `inconclusive`. Keeping the variant owes a `fold`. A test staged in this session and not yet published is simply taken back off it."
|
|
39474
40469
|
},
|
|
39475
40470
|
args: {
|
|
39476
|
-
id:
|
|
39477
|
-
|
|
39478
|
-
type: "
|
|
39479
|
-
description: "
|
|
39480
|
-
required:
|
|
40471
|
+
id: ID_ARG,
|
|
40472
|
+
keep: {
|
|
40473
|
+
type: "string",
|
|
40474
|
+
description: "Which version stays live: `original` or `variant`.",
|
|
40475
|
+
required: true
|
|
40476
|
+
},
|
|
40477
|
+
reason: REASON_ARG,
|
|
40478
|
+
learning: LEARNING_ARG
|
|
40479
|
+
},
|
|
40480
|
+
run: async ({ args }) => {
|
|
40481
|
+
const keep = String(args.keep);
|
|
40482
|
+
if (keep !== "original" && keep !== "variant") {
|
|
40483
|
+
fail5("`--keep` is either `original` or `variant` \u2014 which version should everyone see from now on?");
|
|
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?`);
|
|
39481
40488
|
}
|
|
40489
|
+
try {
|
|
40490
|
+
const response = await apiPost(
|
|
40491
|
+
"/api/experiments/finish",
|
|
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."
|
|
39482
40523
|
},
|
|
40524
|
+
args: { id: ID_ARG },
|
|
39483
40525
|
run: async ({ args }) => {
|
|
39484
40526
|
try {
|
|
39485
|
-
const response = await apiPost(
|
|
39486
|
-
|
|
39487
|
-
|
|
40527
|
+
const response = await apiPost(
|
|
40528
|
+
"/api/experiments/finish",
|
|
40529
|
+
{ experimentId: String(args.id), keep: "original" }
|
|
40530
|
+
);
|
|
40531
|
+
if ("unstaged" in response.data) {
|
|
40532
|
+
writeJsonEnvelope({
|
|
40533
|
+
ok: true,
|
|
40534
|
+
data: response.data,
|
|
40535
|
+
hints: ["That test had not been published yet, so it was taken back off this session. Nothing was split."]
|
|
40536
|
+
});
|
|
40537
|
+
return;
|
|
40538
|
+
}
|
|
40539
|
+
const data = response.data;
|
|
40540
|
+
writeJsonEnvelope({
|
|
40541
|
+
ok: true,
|
|
40542
|
+
data,
|
|
40543
|
+
hints: [
|
|
40544
|
+
"Ended. Everyone sees the original page now, and the split stopped within seconds.",
|
|
40545
|
+
...data.verdict === "keep_running" ? [
|
|
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.`
|
|
40549
|
+
] : [],
|
|
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
|
+
]
|
|
39488
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);
|
|
39489
40600
|
writeJsonEnvelope({
|
|
39490
40601
|
ok: true,
|
|
39491
40602
|
data: response.data,
|
|
39492
40603
|
hints: [
|
|
39493
|
-
|
|
39494
|
-
|
|
39495
|
-
`You abandoned a test that HAD concluded (${response.data.discardedVerdict}). The original page keeps the traffic and no result is recorded \u2014 the finding is gone. If that was not what you meant, run this again without --abandon.`
|
|
39496
|
-
] : [],
|
|
39497
|
-
"Then run `baker experiment fold` \u2014 it moves the winning version into the page itself and archives the other one, so the page's own content is what visitors get. Until that lands the page is served by a rewrite, and it cannot be tested again."
|
|
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}`
|
|
39498
40606
|
]
|
|
39499
40607
|
});
|
|
39500
40608
|
} catch (error) {
|
|
@@ -39502,6 +40610,56 @@ var finishCommand = defineCommand119({
|
|
|
39502
40610
|
}
|
|
39503
40611
|
}
|
|
39504
40612
|
});
|
|
40613
|
+
registerSchema({
|
|
40614
|
+
command: "experiment.pause",
|
|
40615
|
+
description: "Switch a test's split off \u2014 everyone sees the original until it is resumed",
|
|
40616
|
+
args: { id: ID_ARG }
|
|
40617
|
+
});
|
|
40618
|
+
var pauseCommand = defineCommand119({
|
|
40619
|
+
meta: {
|
|
40620
|
+
name: "pause",
|
|
40621
|
+
description: "Switch the split off now. Everyone sees the original; visitors already assigned keep their side, so resuming continues the same test rather than starting a new one. Use it when the new variant has to come down for a while \u2014 a broken price, a campaign that needs the original \u2014 without throwing the test away."
|
|
40622
|
+
},
|
|
40623
|
+
args: { id: ID_ARG },
|
|
40624
|
+
run: async ({ args }) => {
|
|
40625
|
+
try {
|
|
40626
|
+
await apiPost("/api/experiments/pause", { experimentId: String(args.id) });
|
|
40627
|
+
writeJsonEnvelope({
|
|
40628
|
+
ok: true,
|
|
40629
|
+
data: { paused: true },
|
|
40630
|
+
hints: [
|
|
40631
|
+
`Paused. Everyone sees the original page now. \`baker experiment resume --id ${String(args.id)}\` restarts the split with the same sides.`
|
|
40632
|
+
]
|
|
40633
|
+
});
|
|
40634
|
+
} catch (error) {
|
|
40635
|
+
reportApiError(error);
|
|
40636
|
+
}
|
|
40637
|
+
}
|
|
40638
|
+
});
|
|
40639
|
+
registerSchema({
|
|
40640
|
+
command: "experiment.resume",
|
|
40641
|
+
description: "Switch a paused test's split back on",
|
|
40642
|
+
args: { id: ID_ARG }
|
|
40643
|
+
});
|
|
40644
|
+
var resumeCommand = defineCommand119({
|
|
40645
|
+
meta: {
|
|
40646
|
+
name: "resume",
|
|
40647
|
+
description: "Switch a paused test's split back on. Visitors already assigned see the same side they saw before."
|
|
40648
|
+
},
|
|
40649
|
+
args: { id: ID_ARG },
|
|
40650
|
+
run: async ({ args }) => {
|
|
40651
|
+
try {
|
|
40652
|
+
await apiPost("/api/experiments/resume", { experimentId: String(args.id) });
|
|
40653
|
+
writeJsonEnvelope({
|
|
40654
|
+
ok: true,
|
|
40655
|
+
data: { resumed: true },
|
|
40656
|
+
hints: ["Resumed. The split is back on, with the same sides."]
|
|
40657
|
+
});
|
|
40658
|
+
} catch (error) {
|
|
40659
|
+
reportApiError(error);
|
|
40660
|
+
}
|
|
40661
|
+
}
|
|
40662
|
+
});
|
|
39505
40663
|
registerSchema({
|
|
39506
40664
|
command: "experiment.fold",
|
|
39507
40665
|
description: "Make the page's own content the version that won, and archive the one that lost",
|
|
@@ -39541,6 +40699,7 @@ var foldCommand = defineCommand119({
|
|
|
39541
40699
|
if (folded.length > 0) {
|
|
39542
40700
|
await apiPost("/api/experiments/fold", {
|
|
39543
40701
|
applied: true,
|
|
40702
|
+
experimentIds: folded.map((result) => result.experimentId),
|
|
39544
40703
|
...args.id ? { experimentId: String(args.id) } : {}
|
|
39545
40704
|
});
|
|
39546
40705
|
}
|
|
@@ -39563,13 +40722,18 @@ var foldCommand = defineCommand119({
|
|
|
39563
40722
|
var experimentCommand = defineCommand119({
|
|
39564
40723
|
meta: {
|
|
39565
40724
|
name: "experiment",
|
|
39566
|
-
description: "Run an A/B test between a landing page and
|
|
40725
|
+
description: "Run an A/B test between a landing page and a new variant of it, and be told which one is better. Always `plan` first: most pages do not get enough traffic to settle most questions, and a test that cannot conclude still produces a number somebody will act on. Nothing ends a test on its own \u2014 `finish` does, when you or the client decide."
|
|
39567
40726
|
},
|
|
39568
40727
|
subCommands: {
|
|
39569
40728
|
plan: planCommand,
|
|
39570
40729
|
start: startCommand,
|
|
40730
|
+
update: updateCommand3,
|
|
39571
40731
|
status: statusCommand6,
|
|
40732
|
+
history: historyCommand,
|
|
40733
|
+
pause: pauseCommand,
|
|
40734
|
+
resume: resumeCommand,
|
|
39572
40735
|
finish: finishCommand,
|
|
40736
|
+
cancel: cancelCommand,
|
|
39573
40737
|
fold: foldCommand
|
|
39574
40738
|
}
|
|
39575
40739
|
});
|
|
@@ -39580,176 +40744,6 @@ import { defineCommand as defineCommand124 } from "citty";
|
|
|
39580
40744
|
// src/commands/flows/add.ts
|
|
39581
40745
|
import { randomUUID } from "crypto";
|
|
39582
40746
|
import { defineCommand as defineCommand120 } from "citty";
|
|
39583
|
-
|
|
39584
|
-
// src/commands/flows/shared.ts
|
|
39585
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
|
|
39586
|
-
import { join as join3 } from "path";
|
|
39587
|
-
var FLOWS_DIR = "src/lib/flow-engine/flows";
|
|
39588
|
-
function flowsDir() {
|
|
39589
|
-
let dir = process.cwd();
|
|
39590
|
-
for (let i = 0; i < 6; i++) {
|
|
39591
|
-
const candidate = join3(dir, FLOWS_DIR);
|
|
39592
|
-
if (existsSync5(candidate)) {
|
|
39593
|
-
return candidate;
|
|
39594
|
-
}
|
|
39595
|
-
const parent = join3(dir, "..");
|
|
39596
|
-
if (parent === dir) {
|
|
39597
|
-
break;
|
|
39598
|
-
}
|
|
39599
|
-
dir = parent;
|
|
39600
|
-
}
|
|
39601
|
-
return join3(process.cwd(), FLOWS_DIR);
|
|
39602
|
-
}
|
|
39603
|
-
function failLocal(message) {
|
|
39604
|
-
writeJson({ ok: false, error: { code: "NOT_FOUND", message } });
|
|
39605
|
-
process.exit(1);
|
|
39606
|
-
}
|
|
39607
|
-
function listFlowSlugs() {
|
|
39608
|
-
const dir = flowsDir();
|
|
39609
|
-
if (!existsSync5(dir)) {
|
|
39610
|
-
return [];
|
|
39611
|
-
}
|
|
39612
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
|
|
39613
|
-
}
|
|
39614
|
-
function flowDataPath(slug) {
|
|
39615
|
-
return join3(flowsDir(), slug, "_data.json");
|
|
39616
|
-
}
|
|
39617
|
-
function writeFlowTree(slug, tree) {
|
|
39618
|
-
writeFileSync3(flowDataPath(slug), `${JSON.stringify(tree, null, 2)}
|
|
39619
|
-
`, "utf-8");
|
|
39620
|
-
}
|
|
39621
|
-
function walkNodes(node, acc = []) {
|
|
39622
|
-
if (!node || typeof node !== "object") return acc;
|
|
39623
|
-
acc.push(node);
|
|
39624
|
-
if (Array.isArray(node.children)) {
|
|
39625
|
-
for (const child of node.children) walkNodes(child, acc);
|
|
39626
|
-
}
|
|
39627
|
-
return acc;
|
|
39628
|
-
}
|
|
39629
|
-
function collectSideEffects(tree) {
|
|
39630
|
-
return walkNodes(tree).flatMap(
|
|
39631
|
-
(node) => Array.isArray(node.sideEffects) ? node.sideEffects.filter((sideEffect) => Boolean(sideEffect) && typeof sideEffect === "object").map((sideEffect) => ({ node, sideEffect })) : []
|
|
39632
|
-
);
|
|
39633
|
-
}
|
|
39634
|
-
function readFlowTree(slug) {
|
|
39635
|
-
const path42 = join3(flowsDir(), slug, "_data.json");
|
|
39636
|
-
if (!existsSync5(path42)) {
|
|
39637
|
-
failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
|
|
39638
|
-
}
|
|
39639
|
-
try {
|
|
39640
|
-
return JSON.parse(readFileSync11(path42, "utf-8"));
|
|
39641
|
-
} catch (error) {
|
|
39642
|
-
failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
39643
|
-
}
|
|
39644
|
-
}
|
|
39645
|
-
function isConfigured(value) {
|
|
39646
|
-
return typeof value === "string" && value.length > 0;
|
|
39647
|
-
}
|
|
39648
|
-
function nodeMeta(record) {
|
|
39649
|
-
const nodeId = typeof record.id === "string" ? record.id : "";
|
|
39650
|
-
const nodeName = typeof record.name === "string" ? record.name : void 0;
|
|
39651
|
-
return { nodeId, ...nodeName ? { nodeName } : {} };
|
|
39652
|
-
}
|
|
39653
|
-
function widgetNodeStatus(record, meta) {
|
|
39654
|
-
const nodeData = record.nodeData;
|
|
39655
|
-
const nodeType = nodeData?.type;
|
|
39656
|
-
if (typeof nodeType !== "string" || !FLOW_RESOURCE_NODE_TYPES.includes(nodeType)) {
|
|
39657
|
-
return null;
|
|
39658
|
-
}
|
|
39659
|
-
const external = nodeData?.form?.external;
|
|
39660
|
-
const selected = external != null && typeof external === "object";
|
|
39661
|
-
const name = selected ? external.name : void 0;
|
|
39662
|
-
return {
|
|
39663
|
-
...meta,
|
|
39664
|
-
target: "node",
|
|
39665
|
-
kind: nodeType,
|
|
39666
|
-
status: selected ? `${nodeType} \u2014 selected${typeof name === "string" ? ` "${name}"` : ""}` : `${nodeType} \u2014 not selected`,
|
|
39667
|
-
needsInput: !selected
|
|
39668
|
-
};
|
|
39669
|
-
}
|
|
39670
|
-
function sideEffectNeedsInput(raw) {
|
|
39671
|
-
const effect = raw;
|
|
39672
|
-
const type = effect?.type;
|
|
39673
|
-
if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
|
|
39674
|
-
return false;
|
|
39675
|
-
}
|
|
39676
|
-
const configured = isConfigured(effect?.encryptedConfig);
|
|
39677
|
-
if (!isFlowOauthSideEffect(type)) return !configured;
|
|
39678
|
-
return !isConfigured(effect?.oauthProviderId) || !configured;
|
|
39679
|
-
}
|
|
39680
|
-
function sideEffectStatus(raw, meta) {
|
|
39681
|
-
const effect = raw;
|
|
39682
|
-
const type = effect?.type;
|
|
39683
|
-
if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
|
|
39684
|
-
return null;
|
|
39685
|
-
}
|
|
39686
|
-
const configured = isConfigured(effect?.encryptedConfig);
|
|
39687
|
-
const oauth = isFlowOauthSideEffect(type);
|
|
39688
|
-
const connected = isConfigured(effect?.oauthProviderId);
|
|
39689
|
-
const needs = FLOW_SECRET_FIELDS[type];
|
|
39690
|
-
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(", ")})` : ""}`;
|
|
39691
|
-
return {
|
|
39692
|
-
...meta,
|
|
39693
|
-
target: "sideEffect",
|
|
39694
|
-
...typeof effect?.id === "string" ? { sideEffectId: effect.id } : {},
|
|
39695
|
-
kind: type,
|
|
39696
|
-
status,
|
|
39697
|
-
needsInput: sideEffectNeedsInput(effect)
|
|
39698
|
-
};
|
|
39699
|
-
}
|
|
39700
|
-
function requestFlowInputCall(slug, status) {
|
|
39701
|
-
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}" }`;
|
|
39702
|
-
return `request_flow_input with ${args}`;
|
|
39703
|
-
}
|
|
39704
|
-
function collectConfigStatus(node, acc) {
|
|
39705
|
-
if (!node || typeof node !== "object") {
|
|
39706
|
-
return;
|
|
39707
|
-
}
|
|
39708
|
-
const record = node;
|
|
39709
|
-
const meta = nodeMeta(record);
|
|
39710
|
-
const widget = widgetNodeStatus(record, meta);
|
|
39711
|
-
if (widget) {
|
|
39712
|
-
acc.push(widget);
|
|
39713
|
-
}
|
|
39714
|
-
if (Array.isArray(record.sideEffects)) {
|
|
39715
|
-
for (const raw of record.sideEffects) {
|
|
39716
|
-
const status = sideEffectStatus(raw, meta);
|
|
39717
|
-
if (status) {
|
|
39718
|
-
acc.push(status);
|
|
39719
|
-
}
|
|
39720
|
-
}
|
|
39721
|
-
}
|
|
39722
|
-
if (Array.isArray(record.children)) {
|
|
39723
|
-
for (const child of record.children) {
|
|
39724
|
-
collectConfigStatus(child, acc);
|
|
39725
|
-
}
|
|
39726
|
-
}
|
|
39727
|
-
}
|
|
39728
|
-
function redactTree(node) {
|
|
39729
|
-
if (Array.isArray(node)) {
|
|
39730
|
-
return node.map(redactTree);
|
|
39731
|
-
}
|
|
39732
|
-
if (!node || typeof node !== "object") {
|
|
39733
|
-
return node;
|
|
39734
|
-
}
|
|
39735
|
-
const out = {};
|
|
39736
|
-
for (const [key, value] of Object.entries(node)) {
|
|
39737
|
-
if (key === "encryptedConfig") {
|
|
39738
|
-
out[key] = isConfigured(value) ? "[configured]" : value;
|
|
39739
|
-
} else {
|
|
39740
|
-
out[key] = redactTree(value);
|
|
39741
|
-
}
|
|
39742
|
-
}
|
|
39743
|
-
return out;
|
|
39744
|
-
}
|
|
39745
|
-
function stepNameKey(name) {
|
|
39746
|
-
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
|
|
39747
|
-
}
|
|
39748
|
-
function isUsableStepNameKey(key) {
|
|
39749
|
-
return /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(key);
|
|
39750
|
-
}
|
|
39751
|
-
|
|
39752
|
-
// src/commands/flows/add.ts
|
|
39753
40747
|
function unknownType(kind, type) {
|
|
39754
40748
|
writeJson({
|
|
39755
40749
|
ok: false,
|
|
@@ -39840,6 +40834,10 @@ var addNodeCommand = defineCommand120({
|
|
|
39840
40834
|
data: { slug, nodeId: node.id, type, parentId: parent.id ?? null, path: flowDataPath(slug) },
|
|
39841
40835
|
hints: [
|
|
39842
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.`,
|
|
39843
40841
|
...entry.owners?.["nodeData.form.external"] === "request_flow_input" ? [
|
|
39844
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}" }.`
|
|
39845
40843
|
] : [],
|
|
@@ -39953,6 +40951,13 @@ var addSideEffectCommand = defineCommand120({
|
|
|
39953
40951
|
data: { slug, nodeId: node.id, sideEffectId: id, type, triggerId, path: flowDataPath(slug) },
|
|
39954
40952
|
hints: [
|
|
39955
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
|
+
] : [],
|
|
39956
40961
|
...needsUser.some(([, owner]) => owner === "request_flow_input") ? [
|
|
39957
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.`
|
|
39958
40963
|
] : [],
|
|
@@ -39966,11 +40971,11 @@ var addSideEffectCommand = defineCommand120({
|
|
|
39966
40971
|
});
|
|
39967
40972
|
|
|
39968
40973
|
// src/commands/flows/map.ts
|
|
39969
|
-
import { readFileSync as
|
|
40974
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
39970
40975
|
import { defineCommand as defineCommand121 } from "citty";
|
|
39971
40976
|
|
|
39972
40977
|
// src/commands/flows/value-expression.ts
|
|
39973
|
-
import { existsSync as existsSync6, readFileSync as
|
|
40978
|
+
import { existsSync as existsSync6, readFileSync as readFileSync13 } from "fs";
|
|
39974
40979
|
import { join as join4 } from "path";
|
|
39975
40980
|
var PREFIXES = ["field", "tracking", "global", "code"];
|
|
39976
40981
|
function splitParts(raw) {
|
|
@@ -40029,10 +41034,10 @@ function parseValueExpression(raw) {
|
|
|
40029
41034
|
return parts.map(parsePart);
|
|
40030
41035
|
}
|
|
40031
41036
|
function trackingFieldIds() {
|
|
40032
|
-
const
|
|
40033
|
-
if (!existsSync6(
|
|
41037
|
+
const path44 = join4(flowsDir(), "..", "tracking.ts");
|
|
41038
|
+
if (!existsSync6(path44)) return null;
|
|
40034
41039
|
try {
|
|
40035
|
-
const source =
|
|
41040
|
+
const source = readFileSync13(path44, "utf-8");
|
|
40036
41041
|
const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
|
|
40037
41042
|
if (!block2) return null;
|
|
40038
41043
|
const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
|
|
@@ -40573,13 +41578,13 @@ function specsFromFile(parsed) {
|
|
|
40573
41578
|
return `${destField}${type}=${entry?.value ?? ""}`;
|
|
40574
41579
|
});
|
|
40575
41580
|
}
|
|
40576
|
-
function readSpecFile(
|
|
41581
|
+
function readSpecFile(path44) {
|
|
40577
41582
|
let raw;
|
|
40578
41583
|
try {
|
|
40579
|
-
raw =
|
|
41584
|
+
raw = path44 === "-" ? readFileSync14(0, "utf-8") : readFileSync14(path44, "utf-8");
|
|
40580
41585
|
} catch (error) {
|
|
40581
41586
|
refuse(
|
|
40582
|
-
`Could not read ${
|
|
41587
|
+
`Could not read ${path44 === "-" ? "the mapping from stdin" : `"${path44}"`}: ${error instanceof Error ? error.message : String(error)}`
|
|
40583
41588
|
);
|
|
40584
41589
|
}
|
|
40585
41590
|
let parsed;
|
|
@@ -40587,7 +41592,7 @@ function readSpecFile(path42) {
|
|
|
40587
41592
|
parsed = JSON.parse(raw);
|
|
40588
41593
|
} catch (error) {
|
|
40589
41594
|
refuse(
|
|
40590
|
-
`${
|
|
41595
|
+
`${path44 === "-" ? "stdin" : `"${path44}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
40591
41596
|
'Expected { "map": { "<destField>": "<value>", \u2026 } }'
|
|
40592
41597
|
);
|
|
40593
41598
|
}
|
|
@@ -40857,18 +41862,18 @@ var ARRAY_FIELDS = [
|
|
|
40857
41862
|
"tagIds"
|
|
40858
41863
|
];
|
|
40859
41864
|
var ARRAY_OWNERS = ["", "body"];
|
|
40860
|
-
function dropUnsetOptionals(sideEffect,
|
|
41865
|
+
function dropUnsetOptionals(sideEffect, path44) {
|
|
40861
41866
|
return OPTIONAL_STRINGS.flatMap((key) => {
|
|
40862
41867
|
if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
|
|
40863
41868
|
delete sideEffect[key];
|
|
40864
|
-
return [{ path:
|
|
41869
|
+
return [{ path: path44, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
|
|
40865
41870
|
});
|
|
40866
41871
|
}
|
|
40867
|
-
function fillNulledArrays(target, prefix,
|
|
41872
|
+
function fillNulledArrays(target, prefix, path44) {
|
|
40868
41873
|
return ARRAY_FIELDS.flatMap((key) => {
|
|
40869
41874
|
if (!(key in target) || target[key] !== null) return [];
|
|
40870
41875
|
target[key] = [];
|
|
40871
|
-
return [{ path:
|
|
41876
|
+
return [{ path: path44, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
|
|
40872
41877
|
});
|
|
40873
41878
|
}
|
|
40874
41879
|
function sideEffectsOf(node) {
|
|
@@ -40878,13 +41883,13 @@ function sideEffectsOf(node) {
|
|
|
40878
41883
|
);
|
|
40879
41884
|
}
|
|
40880
41885
|
function normalizeSideEffect(sideEffect, where) {
|
|
40881
|
-
const
|
|
41886
|
+
const path44 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
|
|
40882
41887
|
const arrays = ARRAY_OWNERS.flatMap((owner) => {
|
|
40883
41888
|
const target = owner ? sideEffect[owner] : sideEffect;
|
|
40884
41889
|
if (!target || typeof target !== "object") return [];
|
|
40885
|
-
return fillNulledArrays(target, owner ? `${owner}.` : "",
|
|
41890
|
+
return fillNulledArrays(target, owner ? `${owner}.` : "", path44);
|
|
40886
41891
|
});
|
|
40887
|
-
return [...dropUnsetOptionals(sideEffect,
|
|
41892
|
+
return [...dropUnsetOptionals(sideEffect, path44), ...arrays];
|
|
40888
41893
|
}
|
|
40889
41894
|
function normalizeFlowTree(tree) {
|
|
40890
41895
|
const changes = [];
|
|
@@ -41117,9 +42122,15 @@ var listCommand13 = defineCommand124({
|
|
|
41117
42122
|
writeJson({
|
|
41118
42123
|
ok: true,
|
|
41119
42124
|
data: { flows },
|
|
41120
|
-
hints:
|
|
41121
|
-
|
|
41122
|
-
|
|
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
|
+
]
|
|
41123
42134
|
});
|
|
41124
42135
|
}
|
|
41125
42136
|
});
|
|
@@ -41147,7 +42158,15 @@ var showCommand4 = defineCommand124({
|
|
|
41147
42158
|
data: response,
|
|
41148
42159
|
// Inert side effects first: an unconfigured destination is work still to
|
|
41149
42160
|
// do, but a side effect wired to the wrong moment reads as done.
|
|
41150
|
-
hints: [
|
|
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
|
+
]
|
|
41151
42170
|
});
|
|
41152
42171
|
}
|
|
41153
42172
|
});
|
|
@@ -41312,7 +42331,7 @@ Examples:
|
|
|
41312
42331
|
import { defineCommand as defineCommand126 } from "citty";
|
|
41313
42332
|
|
|
41314
42333
|
// src/commands/ga4/shared.ts
|
|
41315
|
-
import { readFileSync as
|
|
42334
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
41316
42335
|
function failValidation3(message) {
|
|
41317
42336
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
41318
42337
|
process.exit(1);
|
|
@@ -41328,7 +42347,7 @@ function requireTarget5(args, entity) {
|
|
|
41328
42347
|
function readJsonSource(args, flag) {
|
|
41329
42348
|
const inline = args[flag];
|
|
41330
42349
|
const file = args.file;
|
|
41331
|
-
const raw = typeof file === "string" && file.length > 0 ?
|
|
42350
|
+
const raw = typeof file === "string" && file.length > 0 ? readFileSync15(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
|
|
41332
42351
|
if (raw === void 0) {
|
|
41333
42352
|
failValidation3(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
|
|
41334
42353
|
}
|
|
@@ -41422,10 +42441,10 @@ async function stageOps(ops) {
|
|
|
41422
42441
|
handleError2(err);
|
|
41423
42442
|
}
|
|
41424
42443
|
}
|
|
41425
|
-
async function draftAction2(
|
|
42444
|
+
async function draftAction2(path44, body, chat) {
|
|
41426
42445
|
const chatId = resolveChatId(chat);
|
|
41427
42446
|
try {
|
|
41428
|
-
const data = await apiPost(
|
|
42447
|
+
const data = await apiPost(path44, { chatId, ...body });
|
|
41429
42448
|
writeJsonEnvelope({ ok: true, data });
|
|
41430
42449
|
return data;
|
|
41431
42450
|
} catch (err) {
|
|
@@ -41679,7 +42698,7 @@ Examples:
|
|
|
41679
42698
|
});
|
|
41680
42699
|
|
|
41681
42700
|
// src/commands/ga4/query.ts
|
|
41682
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as
|
|
42701
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync4 } from "fs";
|
|
41683
42702
|
import { resolve as resolve2 } from "path";
|
|
41684
42703
|
import { defineCommand as defineCommand129 } from "citty";
|
|
41685
42704
|
|
|
@@ -41770,7 +42789,7 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
41770
42789
|
writeFileSync4(filePath, content, "utf-8");
|
|
41771
42790
|
}
|
|
41772
42791
|
} else if (append && existsSync7(filePath)) {
|
|
41773
|
-
const existing = JSON.parse(
|
|
42792
|
+
const existing = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
41774
42793
|
writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
41775
42794
|
} else {
|
|
41776
42795
|
writeFileSync4(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
@@ -42622,7 +43641,7 @@ Full guide: __tooling__/docs/tools/baker/ga4.md`
|
|
|
42622
43641
|
import { defineCommand as defineCommand135 } from "citty";
|
|
42623
43642
|
|
|
42624
43643
|
// src/commands/gsc/query.ts
|
|
42625
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as
|
|
43644
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
|
|
42626
43645
|
import { resolve as resolve3 } from "path";
|
|
42627
43646
|
import { defineCommand as defineCommand132 } from "citty";
|
|
42628
43647
|
|
|
@@ -42770,7 +43789,7 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
42770
43789
|
writeFileSync5(filePath, content, "utf-8");
|
|
42771
43790
|
}
|
|
42772
43791
|
} else if (append && existsSync8(filePath)) {
|
|
42773
|
-
const existing = JSON.parse(
|
|
43792
|
+
const existing = JSON.parse(readFileSync17(filePath, "utf-8"));
|
|
42774
43793
|
writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
42775
43794
|
} else {
|
|
42776
43795
|
writeFileSync5(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
@@ -43147,7 +44166,7 @@ var listCommand14 = defineCommand136({
|
|
|
43147
44166
|
}
|
|
43148
44167
|
}
|
|
43149
44168
|
});
|
|
43150
|
-
var
|
|
44169
|
+
var historyCommand2 = defineCommand136({
|
|
43151
44170
|
meta: {
|
|
43152
44171
|
name: "history",
|
|
43153
44172
|
description: `Unified account history (audit log): what changed, who did it, and when.
|
|
@@ -43839,7 +44858,7 @@ function cropSprite(input, region) {
|
|
|
43839
44858
|
|
|
43840
44859
|
// src/lib/image/io.ts
|
|
43841
44860
|
import { randomBytes } from "crypto";
|
|
43842
|
-
import { glob as fsGlob, readFile as
|
|
44861
|
+
import { glob as fsGlob, readFile as readFile24, rename, stat as stat6, writeFile as writeFile12 } from "fs/promises";
|
|
43843
44862
|
import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
|
|
43844
44863
|
var REMOTE_RE = /^https?:\/\//i;
|
|
43845
44864
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -43872,11 +44891,11 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
43872
44891
|
const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
|
|
43873
44892
|
return buffer;
|
|
43874
44893
|
}
|
|
43875
|
-
return
|
|
44894
|
+
return readFile24(pathOrUrl);
|
|
43876
44895
|
}
|
|
43877
|
-
async function isDirectory(
|
|
44896
|
+
async function isDirectory(path44) {
|
|
43878
44897
|
try {
|
|
43879
|
-
const s = await stat6(
|
|
44898
|
+
const s = await stat6(path44);
|
|
43880
44899
|
return s.isDirectory();
|
|
43881
44900
|
} catch {
|
|
43882
44901
|
return false;
|
|
@@ -44195,13 +45214,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
|
|
|
44195
45214
|
}
|
|
44196
45215
|
function disambiguate(paths) {
|
|
44197
45216
|
const taken = /* @__PURE__ */ new Set();
|
|
44198
|
-
return paths.map((
|
|
44199
|
-
if (!taken.has(
|
|
44200
|
-
taken.add(
|
|
44201
|
-
return
|
|
45217
|
+
return paths.map((path44) => {
|
|
45218
|
+
if (!taken.has(path44)) {
|
|
45219
|
+
taken.add(path44);
|
|
45220
|
+
return path44;
|
|
44202
45221
|
}
|
|
44203
|
-
const ext = extname3(
|
|
44204
|
-
const stem =
|
|
45222
|
+
const ext = extname3(path44);
|
|
45223
|
+
const stem = path44.slice(0, path44.length - ext.length);
|
|
44205
45224
|
let n = 2;
|
|
44206
45225
|
while (taken.has(`${stem}-${n}${ext}`)) n += 1;
|
|
44207
45226
|
const unique = `${stem}-${n}${ext}`;
|
|
@@ -44327,10 +45346,10 @@ async function runDownloads(plan) {
|
|
|
44327
45346
|
const paths = disambiguate(fetched.map((item) => item.path));
|
|
44328
45347
|
const downloaded = [];
|
|
44329
45348
|
for (const [index, item] of fetched.entries()) {
|
|
44330
|
-
const
|
|
45349
|
+
const path44 = paths[index] ?? item.path;
|
|
44331
45350
|
try {
|
|
44332
|
-
await atomicWrite(
|
|
44333
|
-
downloaded.push({ input: item.input, output:
|
|
45351
|
+
await atomicWrite(path44, item.buffer);
|
|
45352
|
+
downloaded.push({ input: item.input, output: path44, bytes: item.buffer.length, contentType: item.contentType });
|
|
44334
45353
|
} catch (err) {
|
|
44335
45354
|
failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
|
|
44336
45355
|
}
|
|
@@ -47294,8 +48313,8 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
47294
48313
|
import { defineCommand as defineCommand175 } from "citty";
|
|
47295
48314
|
|
|
47296
48315
|
// src/commands/landing/critique.ts
|
|
47297
|
-
import { readdir as
|
|
47298
|
-
import
|
|
48316
|
+
import { readdir as readdir12, readFile as readFile26, stat as stat8 } from "fs/promises";
|
|
48317
|
+
import path33 from "path";
|
|
47299
48318
|
import { defineCommand as defineCommand164 } from "citty";
|
|
47300
48319
|
|
|
47301
48320
|
// src/engine/landing/lib/constants.ts
|
|
@@ -48473,13 +49492,13 @@ function describeCounts(findings) {
|
|
|
48473
49492
|
|
|
48474
49493
|
// src/commands/landing/snapshot.ts
|
|
48475
49494
|
import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
|
|
48476
|
-
import
|
|
49495
|
+
import path31 from "path";
|
|
48477
49496
|
var CRITIC_VERSION = "3";
|
|
48478
49497
|
function critiqueCacheDir(projectRoot) {
|
|
48479
|
-
return
|
|
49498
|
+
return path31.join(projectRoot, ".cache", "landing-critique");
|
|
48480
49499
|
}
|
|
48481
49500
|
function snapshotPath(projectRoot, slug) {
|
|
48482
|
-
return
|
|
49501
|
+
return path31.join(critiqueCacheDir(projectRoot), `${slug}.json`);
|
|
48483
49502
|
}
|
|
48484
49503
|
async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
48485
49504
|
await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
|
|
@@ -48491,8 +49510,8 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
|
48491
49510
|
}
|
|
48492
49511
|
|
|
48493
49512
|
// src/commands/landing/source-version.ts
|
|
48494
|
-
import { readFile as
|
|
48495
|
-
import
|
|
49513
|
+
import { readdir as readdir11, readFile as readFile25, stat as stat7 } from "fs/promises";
|
|
49514
|
+
import path32 from "path";
|
|
48496
49515
|
var CRITIQUED_ROOTS = ["src/pages/", "src/components/"];
|
|
48497
49516
|
async function landingSourceRelPaths(root, slug) {
|
|
48498
49517
|
const files = await landingGraphFiles(root, slug);
|
|
@@ -48501,7 +49520,7 @@ async function landingSourceRelPaths(root, slug) {
|
|
|
48501
49520
|
async function readLandingSources(root, slug) {
|
|
48502
49521
|
const rel = await landingSourceRelPaths(root, slug);
|
|
48503
49522
|
const out = [];
|
|
48504
|
-
for (const r of rel) out.push({ path: r, text: await
|
|
49523
|
+
for (const r of rel) out.push({ path: r, text: await readFile25(path32.join(root, r), "utf8") });
|
|
48505
49524
|
return out;
|
|
48506
49525
|
}
|
|
48507
49526
|
async function computeLandingSourceSha(root, slug) {
|
|
@@ -48510,7 +49529,7 @@ async function computeLandingSourceSha(root, slug) {
|
|
|
48510
49529
|
for (const r of rel) {
|
|
48511
49530
|
let bytes;
|
|
48512
49531
|
try {
|
|
48513
|
-
bytes = await
|
|
49532
|
+
bytes = await readFile25(path32.join(root, r));
|
|
48514
49533
|
} catch {
|
|
48515
49534
|
bytes = Buffer.alloc(0);
|
|
48516
49535
|
}
|
|
@@ -48518,6 +49537,47 @@ async function computeLandingSourceSha(root, slug) {
|
|
|
48518
49537
|
}
|
|
48519
49538
|
return sha256Hex(Buffer.concat(parts));
|
|
48520
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
|
+
}
|
|
48521
49581
|
|
|
48522
49582
|
// src/commands/landing/critique.ts
|
|
48523
49583
|
registerSchema({
|
|
@@ -48573,7 +49633,7 @@ var critiqueCommand2 = defineCommand164({
|
|
|
48573
49633
|
{ availableSlugs: await listLandingSlugs(projectRoot) }
|
|
48574
49634
|
);
|
|
48575
49635
|
}
|
|
48576
|
-
if (!await isDir2(
|
|
49636
|
+
if (!await isDir2(path33.resolve(projectRoot, "src", "pages", slug))) {
|
|
48577
49637
|
fail6("NOT_FOUND", `No landing at src/pages/${slug}/`, {
|
|
48578
49638
|
availableSlugs: await listLandingSlugs(projectRoot)
|
|
48579
49639
|
});
|
|
@@ -48613,15 +49673,17 @@ var critiqueCommand2 = defineCommand164({
|
|
|
48613
49673
|
}
|
|
48614
49674
|
});
|
|
48615
49675
|
async function critiqueOne(projectRoot, slug, brand, competitors) {
|
|
48616
|
-
const [sources, sourceSha] = await Promise.all([
|
|
49676
|
+
const [sources, compositionSha, sourceSha] = await Promise.all([
|
|
48617
49677
|
readLandingSources(projectRoot, slug),
|
|
48618
|
-
computeLandingSourceSha(projectRoot, slug)
|
|
49678
|
+
computeLandingSourceSha(projectRoot, slug),
|
|
49679
|
+
computeLegacyLandingSourceSha(path33.resolve(projectRoot, "src", "pages", slug))
|
|
48619
49680
|
]);
|
|
48620
49681
|
const report = critiqueLanding({ slug, sources, brand, competitors });
|
|
48621
49682
|
let snapshotFailed = false;
|
|
48622
49683
|
try {
|
|
48623
49684
|
await writeCritiqueSnapshot(projectRoot, {
|
|
48624
49685
|
slug,
|
|
49686
|
+
compositionSha,
|
|
48625
49687
|
sourceSha,
|
|
48626
49688
|
criticVersion: CRITIC_VERSION,
|
|
48627
49689
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -48634,18 +49696,18 @@ async function critiqueOne(projectRoot, slug, brand, competitors) {
|
|
|
48634
49696
|
return { slug, report, snapshotFailed };
|
|
48635
49697
|
}
|
|
48636
49698
|
async function readCompetitorNames(projectRoot) {
|
|
48637
|
-
const dir =
|
|
49699
|
+
const dir = path33.join(projectRoot, "src", "content", "competitors");
|
|
48638
49700
|
let entries;
|
|
48639
49701
|
try {
|
|
48640
|
-
entries = (await
|
|
49702
|
+
entries = (await readdir12(dir)).filter((f) => f.endsWith(".md"));
|
|
48641
49703
|
} catch {
|
|
48642
49704
|
return [];
|
|
48643
49705
|
}
|
|
48644
49706
|
const names = [];
|
|
48645
49707
|
for (const entry of entries) {
|
|
48646
|
-
names.push(
|
|
49708
|
+
names.push(path33.basename(entry, ".md").replace(/[-_]+/g, " "));
|
|
48647
49709
|
try {
|
|
48648
|
-
const head = (await
|
|
49710
|
+
const head = (await readFile26(path33.join(dir, entry), "utf8")).slice(0, 2e3);
|
|
48649
49711
|
const titled = /^\s*(?:title|name)\s*:\s*["']?([^"'\n]+)["']?\s*$/im.exec(head);
|
|
48650
49712
|
if (titled?.[1]) names.push(titled[1].trim());
|
|
48651
49713
|
} catch {
|
|
@@ -48655,7 +49717,7 @@ async function readCompetitorNames(projectRoot) {
|
|
|
48655
49717
|
}
|
|
48656
49718
|
async function listLandingSlugs(projectRoot) {
|
|
48657
49719
|
try {
|
|
48658
|
-
const entries = await
|
|
49720
|
+
const entries = await readdir12(path33.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
48659
49721
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
48660
49722
|
} catch {
|
|
48661
49723
|
return [];
|
|
@@ -48674,7 +49736,7 @@ function present(f) {
|
|
|
48674
49736
|
}
|
|
48675
49737
|
async function isDir2(p) {
|
|
48676
49738
|
try {
|
|
48677
|
-
return (await
|
|
49739
|
+
return (await stat8(p)).isDirectory();
|
|
48678
49740
|
} catch {
|
|
48679
49741
|
return false;
|
|
48680
49742
|
}
|
|
@@ -48807,7 +49869,7 @@ var addCommand = defineCommand165({
|
|
|
48807
49869
|
|
|
48808
49870
|
// src/commands/landing/inspiration/code.ts
|
|
48809
49871
|
import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
|
|
48810
|
-
import
|
|
49872
|
+
import path34 from "path";
|
|
48811
49873
|
import { defineCommand as defineCommand166 } from "citty";
|
|
48812
49874
|
registerSchema({
|
|
48813
49875
|
command: "landing.inspiration.code",
|
|
@@ -48830,9 +49892,9 @@ var codeCommand = defineCommand166({
|
|
|
48830
49892
|
try {
|
|
48831
49893
|
const id = args.id;
|
|
48832
49894
|
const data = await apiGet("/api/landing-inspiration/section-code", { id });
|
|
48833
|
-
const dir =
|
|
49895
|
+
const dir = path34.join(process.cwd(), ".baker", "inspiration", id);
|
|
48834
49896
|
await mkdir10(dir, { recursive: true });
|
|
48835
|
-
const file =
|
|
49897
|
+
const file = path34.join(dir, "section.html");
|
|
48836
49898
|
await writeFile14(file, data.html);
|
|
48837
49899
|
const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
48838
49900
|
const fidelity = fidelityHint(data.fidelity);
|
|
@@ -48842,7 +49904,7 @@ var codeCommand = defineCommand166({
|
|
|
48842
49904
|
ok: true,
|
|
48843
49905
|
data: {
|
|
48844
49906
|
id,
|
|
48845
|
-
file:
|
|
49907
|
+
file: path34.relative(process.cwd(), file),
|
|
48846
49908
|
bytes: data.html.length,
|
|
48847
49909
|
fidelity: data.fidelity,
|
|
48848
49910
|
reproduction_notes: data.reproductionNotes,
|
|
@@ -49274,7 +50336,7 @@ function classifyCaptureFailure(error) {
|
|
|
49274
50336
|
|
|
49275
50337
|
// src/engine/landing-library/run.ts
|
|
49276
50338
|
import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
|
|
49277
|
-
import
|
|
50339
|
+
import path36 from "path";
|
|
49278
50340
|
|
|
49279
50341
|
// ../proxy/src/preflight.ts
|
|
49280
50342
|
import http from "http";
|
|
@@ -49413,7 +50475,7 @@ var inPageCollectUsedCss = (selector) => {
|
|
|
49413
50475
|
}
|
|
49414
50476
|
return false;
|
|
49415
50477
|
};
|
|
49416
|
-
const
|
|
50478
|
+
const walk2 = (rules, sink) => {
|
|
49417
50479
|
for (const rule of Array.from(rules)) {
|
|
49418
50480
|
totalRules++;
|
|
49419
50481
|
if (rule instanceof CSSStyleRule) {
|
|
@@ -49434,7 +50496,7 @@ var inPageCollectUsedCss = (selector) => {
|
|
|
49434
50496
|
const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
|
|
49435
50497
|
if (grouping) {
|
|
49436
50498
|
const inner = [];
|
|
49437
|
-
|
|
50499
|
+
walk2(rule.cssRules, inner);
|
|
49438
50500
|
if (inner.length === 0) continue;
|
|
49439
50501
|
const condition = rule.conditionText ?? "";
|
|
49440
50502
|
const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
|
|
@@ -49448,7 +50510,7 @@ ${inner.join("\n")}
|
|
|
49448
50510
|
const owner = sheet.ownerNode;
|
|
49449
50511
|
if (owner?.hasAttribute?.("data-baker-freeze")) continue;
|
|
49450
50512
|
try {
|
|
49451
|
-
|
|
50513
|
+
walk2(sheet.cssRules, kept);
|
|
49452
50514
|
} catch {
|
|
49453
50515
|
if (sheet.href) unreadableHrefs.push(sheet.href);
|
|
49454
50516
|
}
|
|
@@ -49899,7 +50961,7 @@ var inPageCollectMotion = (selector) => {
|
|
|
49899
50961
|
else if (infinite) loop.push(effect);
|
|
49900
50962
|
else entrance.push(effect);
|
|
49901
50963
|
};
|
|
49902
|
-
const
|
|
50964
|
+
const walk2 = (rules, insideScrollTimeline) => {
|
|
49903
50965
|
for (const rule of Array.from(rules)) {
|
|
49904
50966
|
if (rule instanceof CSSKeyframesRule) {
|
|
49905
50967
|
keyframesByName.set(rule.name, rule.cssText);
|
|
@@ -49917,17 +50979,17 @@ var inPageCollectMotion = (selector) => {
|
|
|
49917
50979
|
respectsReducedMotion = true;
|
|
49918
50980
|
continue;
|
|
49919
50981
|
}
|
|
49920
|
-
|
|
50982
|
+
walk2(rule.cssRules, insideScrollTimeline);
|
|
49921
50983
|
continue;
|
|
49922
50984
|
}
|
|
49923
50985
|
const grouping = rule;
|
|
49924
|
-
if (grouping.cssRules)
|
|
50986
|
+
if (grouping.cssRules) walk2(grouping.cssRules, insideScrollTimeline);
|
|
49925
50987
|
}
|
|
49926
50988
|
};
|
|
49927
50989
|
for (const sheet of Array.from(document.styleSheets)) {
|
|
49928
50990
|
if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
|
|
49929
50991
|
try {
|
|
49930
|
-
|
|
50992
|
+
walk2(sheet.cssRules, false);
|
|
49931
50993
|
} catch {
|
|
49932
50994
|
}
|
|
49933
50995
|
}
|
|
@@ -50690,9 +51752,9 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
|
|
|
50690
51752
|
|
|
50691
51753
|
// src/engine/landing-library/report.ts
|
|
50692
51754
|
import { writeFile as writeFile15 } from "fs/promises";
|
|
50693
|
-
import
|
|
51755
|
+
import path35 from "path";
|
|
50694
51756
|
async function writeCaptureReport(manifest, outDir) {
|
|
50695
|
-
const file =
|
|
51757
|
+
const file = path35.join(outDir, "report.html");
|
|
50696
51758
|
await writeFile15(file, renderReport(manifest));
|
|
50697
51759
|
return file;
|
|
50698
51760
|
}
|
|
@@ -50871,32 +51933,32 @@ async function reproducePage(args) {
|
|
|
50871
51933
|
const { browser, page, outDir, pageUrl, livePageShot } = args;
|
|
50872
51934
|
const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
|
|
50873
51935
|
if (!built) return { bundle: null, fidelity: null };
|
|
50874
|
-
await writeFile16(
|
|
51936
|
+
await writeFile16(path36.join(outDir, "page.html"), built.html);
|
|
50875
51937
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
|
|
50876
51938
|
wholePage: true,
|
|
50877
51939
|
timeoutMs: 6e4
|
|
50878
51940
|
});
|
|
50879
51941
|
if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
|
|
50880
|
-
await writeFile16(
|
|
51942
|
+
await writeFile16(path36.join(outDir, "page-rendered.png"), rendered);
|
|
50881
51943
|
const { score, note } = await scoreFidelity(livePageShot, rendered);
|
|
50882
51944
|
return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
|
|
50883
51945
|
}
|
|
50884
51946
|
async function captureOneSection(args) {
|
|
50885
51947
|
const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
|
|
50886
|
-
const dir =
|
|
51948
|
+
const dir = path36.join(sectionsDir, String(candidate.index).padStart(2, "0"));
|
|
50887
51949
|
await mkdir11(dir, { recursive: true });
|
|
50888
51950
|
const desktop = await captureSection(page, candidate);
|
|
50889
|
-
if (desktop) await writeFile16(
|
|
51951
|
+
if (desktop) await writeFile16(path36.join(dir, "desktop.png"), desktop);
|
|
50890
51952
|
const visualHash = desktop ? await perceptualHash(desktop) : null;
|
|
50891
51953
|
const motion = await collectMotion(page, candidate.selector);
|
|
50892
51954
|
const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
|
|
50893
51955
|
let fidelity = null;
|
|
50894
51956
|
let fidelityNote;
|
|
50895
51957
|
if (built) {
|
|
50896
|
-
await writeFile16(
|
|
51958
|
+
await writeFile16(path36.join(dir, "section.html"), built.html);
|
|
50897
51959
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
|
|
50898
51960
|
if (rendered && desktop) {
|
|
50899
|
-
await writeFile16(
|
|
51961
|
+
await writeFile16(path36.join(dir, "section-rendered.png"), rendered);
|
|
50900
51962
|
const result = await scoreFidelity(desktop, rendered);
|
|
50901
51963
|
fidelity = result.score;
|
|
50902
51964
|
fidelityNote = result.note;
|
|
@@ -50904,9 +51966,9 @@ async function captureOneSection(args) {
|
|
|
50904
51966
|
}
|
|
50905
51967
|
return {
|
|
50906
51968
|
...candidate,
|
|
50907
|
-
desktopShot: desktop ?
|
|
51969
|
+
desktopShot: desktop ? path36.relative(outDir, path36.join(dir, "desktop.png")) : null,
|
|
50908
51970
|
mobileShot: null,
|
|
50909
|
-
bundle: built ?
|
|
51971
|
+
bundle: built ? path36.relative(outDir, path36.join(dir, "section.html")) : null,
|
|
50910
51972
|
fidelity,
|
|
50911
51973
|
...fidelityNote ? { fidelityNote } : {},
|
|
50912
51974
|
...built ? { cssStats: built.stats } : {},
|
|
@@ -50925,9 +51987,9 @@ async function captureMobileShots(args) {
|
|
|
50925
51987
|
for (const section of sections) {
|
|
50926
51988
|
const shot = await captureSectionOnMobile(mobile.page, section);
|
|
50927
51989
|
if (!shot) continue;
|
|
50928
|
-
const file =
|
|
51990
|
+
const file = path36.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
|
|
50929
51991
|
await writeFile16(file, shot);
|
|
50930
|
-
section.mobileShot =
|
|
51992
|
+
section.mobileShot = path36.relative(outDir, file);
|
|
50931
51993
|
}
|
|
50932
51994
|
} finally {
|
|
50933
51995
|
await mobile.context.close();
|
|
@@ -50942,10 +52004,10 @@ async function captureMotionTakes(args) {
|
|
|
50942
52004
|
const filmOne = async (section) => {
|
|
50943
52005
|
const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
|
|
50944
52006
|
if (!take) return;
|
|
50945
|
-
const dir =
|
|
50946
|
-
const file =
|
|
52007
|
+
const dir = path36.join(sectionsDir, String(section.index).padStart(2, "0"));
|
|
52008
|
+
const file = path36.join(dir, "motion-filmstrip.png");
|
|
50947
52009
|
await writeFile16(file, take.filmstrip);
|
|
50948
|
-
section.motionFilmstrip =
|
|
52010
|
+
section.motionFilmstrip = path36.relative(outDir, file);
|
|
50949
52011
|
log(` [${section.index}] ${section.motion.summary}`);
|
|
50950
52012
|
};
|
|
50951
52013
|
const queue = [...moving];
|
|
@@ -51002,7 +52064,7 @@ async function captureAlternateViews(args) {
|
|
|
51002
52064
|
async function reproduceWholePage(args) {
|
|
51003
52065
|
const { browser, page, outDir, pageUrl, withCode, log } = args;
|
|
51004
52066
|
const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
51005
|
-
if (fullPage) await writeFile16(
|
|
52067
|
+
if (fullPage) await writeFile16(path36.join(outDir, "full-page.png"), fullPage);
|
|
51006
52068
|
if (!withCode) return { bundle: null, fidelity: null };
|
|
51007
52069
|
const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
|
|
51008
52070
|
log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
|
|
@@ -51073,7 +52135,7 @@ async function openViaLadder(args) {
|
|
|
51073
52135
|
async function scrapeLanding(options) {
|
|
51074
52136
|
const timeoutMs = options.timeoutMs ?? 45e3;
|
|
51075
52137
|
const log = options.onProgress ?? (() => void 0);
|
|
51076
|
-
const sectionsDir =
|
|
52138
|
+
const sectionsDir = path36.join(options.outDir, "sections");
|
|
51077
52139
|
const nonPublic = refuseNonPublicUrl(options.url);
|
|
51078
52140
|
if (nonPublic) {
|
|
51079
52141
|
throw new BlockedPageError({
|
|
@@ -51135,7 +52197,7 @@ async function scrapeLanding(options) {
|
|
|
51135
52197
|
security: prepared.security,
|
|
51136
52198
|
captureTier: tier
|
|
51137
52199
|
};
|
|
51138
|
-
await writeFile16(
|
|
52200
|
+
await writeFile16(path36.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
51139
52201
|
`);
|
|
51140
52202
|
if (options.report !== false) {
|
|
51141
52203
|
const reportPath = await writeCaptureReport(manifest, options.outDir);
|
|
@@ -51150,28 +52212,28 @@ async function scrapeLanding(options) {
|
|
|
51150
52212
|
|
|
51151
52213
|
// src/commands/landing/inspiration/captureOut.ts
|
|
51152
52214
|
import { existsSync as existsSync9 } from "fs";
|
|
51153
|
-
import
|
|
52215
|
+
import path37 from "path";
|
|
51154
52216
|
var SCRATCH_DIR = ".baker";
|
|
51155
52217
|
function isWithin(parent, target) {
|
|
51156
|
-
const relative =
|
|
51157
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
52218
|
+
const relative = path37.relative(parent, target);
|
|
52219
|
+
return relative === "" || !relative.startsWith("..") && !path37.isAbsolute(relative);
|
|
51158
52220
|
}
|
|
51159
52221
|
function findRepoRoot(from) {
|
|
51160
|
-
let dir =
|
|
52222
|
+
let dir = path37.resolve(from);
|
|
51161
52223
|
for (; ; ) {
|
|
51162
|
-
if (existsSync9(
|
|
51163
|
-
const parent =
|
|
52224
|
+
if (existsSync9(path37.join(dir, ".git"))) return dir;
|
|
52225
|
+
const parent = path37.dirname(dir);
|
|
51164
52226
|
if (parent === dir) return null;
|
|
51165
52227
|
dir = parent;
|
|
51166
52228
|
}
|
|
51167
52229
|
}
|
|
51168
52230
|
function checkCaptureOut(out, options) {
|
|
51169
52231
|
const { cwd, repoRoot } = options;
|
|
51170
|
-
const resolved =
|
|
52232
|
+
const resolved = path37.resolve(cwd, out);
|
|
51171
52233
|
if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
|
|
51172
|
-
const scratch =
|
|
52234
|
+
const scratch = path37.join(repoRoot, SCRATCH_DIR);
|
|
51173
52235
|
if (isWithin(scratch, resolved)) return { ok: true };
|
|
51174
|
-
const suggestion =
|
|
52236
|
+
const suggestion = path37.posix.join(SCRATCH_DIR, "teardowns", path37.basename(resolved) || "capture");
|
|
51175
52237
|
return {
|
|
51176
52238
|
ok: false,
|
|
51177
52239
|
error: {
|
|
@@ -51345,12 +52407,12 @@ var scrapeCommand = defineCommand169({
|
|
|
51345
52407
|
});
|
|
51346
52408
|
|
|
51347
52409
|
// src/commands/landing/inspiration/search.ts
|
|
51348
|
-
import
|
|
52410
|
+
import path39 from "path";
|
|
51349
52411
|
import { defineCommand as defineCommand170 } from "citty";
|
|
51350
52412
|
|
|
51351
52413
|
// src/commands/landing/inspiration/shot.ts
|
|
51352
52414
|
import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
|
|
51353
|
-
import
|
|
52415
|
+
import path38 from "path";
|
|
51354
52416
|
import sharp6 from "sharp";
|
|
51355
52417
|
var READABLE_SHOT = {
|
|
51356
52418
|
maxWidth: 1440,
|
|
@@ -51376,9 +52438,9 @@ async function downloadReadableShot(url, file) {
|
|
|
51376
52438
|
const response = await fetch(url);
|
|
51377
52439
|
if (!response.ok) return null;
|
|
51378
52440
|
const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
|
|
51379
|
-
await mkdir12(
|
|
52441
|
+
await mkdir12(path38.dirname(file), { recursive: true });
|
|
51380
52442
|
await writeFile17(file, shot);
|
|
51381
|
-
return
|
|
52443
|
+
return path38.relative(process.cwd(), file);
|
|
51382
52444
|
} catch {
|
|
51383
52445
|
return null;
|
|
51384
52446
|
}
|
|
@@ -51470,13 +52532,13 @@ function buildSearchBody(args) {
|
|
|
51470
52532
|
return body;
|
|
51471
52533
|
}
|
|
51472
52534
|
async function downloadShots(results) {
|
|
51473
|
-
const dir =
|
|
52535
|
+
const dir = path39.join(process.cwd(), ".baker", "inspiration");
|
|
51474
52536
|
const saved = /* @__PURE__ */ new Map();
|
|
51475
52537
|
await Promise.all(
|
|
51476
52538
|
results.map(async (result) => {
|
|
51477
52539
|
const file = await downloadReadableShot(
|
|
51478
52540
|
result.desktopShotUrl,
|
|
51479
|
-
|
|
52541
|
+
path39.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
|
|
51480
52542
|
);
|
|
51481
52543
|
if (file) saved.set(result.id, file);
|
|
51482
52544
|
})
|
|
@@ -51704,7 +52766,7 @@ var sequencesCommand = defineCommand171({
|
|
|
51704
52766
|
});
|
|
51705
52767
|
|
|
51706
52768
|
// src/commands/landing/inspiration/view.ts
|
|
51707
|
-
import
|
|
52769
|
+
import path40 from "path";
|
|
51708
52770
|
import { defineCommand as defineCommand172 } from "citty";
|
|
51709
52771
|
registerSchema({
|
|
51710
52772
|
command: "landing.inspiration.view",
|
|
@@ -51737,12 +52799,12 @@ var viewCommand2 = defineCommand172({
|
|
|
51737
52799
|
const id = args.id;
|
|
51738
52800
|
const data = await apiGet("/api/landing-inspiration/section", { id });
|
|
51739
52801
|
const section = data.section;
|
|
51740
|
-
const dir =
|
|
52802
|
+
const dir = path40.join(process.cwd(), ".baker", "inspiration", id);
|
|
51741
52803
|
const ext = READABLE_SHOT.extension;
|
|
51742
52804
|
const [desktop, mobile, filmstrip] = await Promise.all([
|
|
51743
|
-
downloadReadableShot(section.desktopShotUrl,
|
|
51744
|
-
downloadReadableShot(section.mobileShotUrl,
|
|
51745
|
-
downloadReadableShot(section.motionFilmstripUrl,
|
|
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}`))
|
|
51746
52808
|
]);
|
|
51747
52809
|
const full = args.full;
|
|
51748
52810
|
const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
@@ -51848,8 +52910,8 @@ Full guide: __tooling__/docs/tools/baker/landing.md`
|
|
|
51848
52910
|
|
|
51849
52911
|
// src/commands/landing/variant.ts
|
|
51850
52912
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
51851
|
-
import { mkdir as mkdir13, readdir as
|
|
51852
|
-
import
|
|
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";
|
|
51853
52915
|
import { defineCommand as defineCommand174 } from "citty";
|
|
51854
52916
|
registerSchema({
|
|
51855
52917
|
command: "landing.variant",
|
|
@@ -51876,7 +52938,7 @@ registerSchema({
|
|
|
51876
52938
|
var SLUG_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
51877
52939
|
async function readInternalId(definitionPath) {
|
|
51878
52940
|
try {
|
|
51879
|
-
const text2 = await
|
|
52941
|
+
const text2 = await readFile27(definitionPath, "utf8");
|
|
51880
52942
|
return /^internalId:\s*"?([^"\s]+)"?\s*$/m.exec(text2)?.[1] ?? null;
|
|
51881
52943
|
} catch {
|
|
51882
52944
|
return null;
|
|
@@ -51885,7 +52947,7 @@ async function readInternalId(definitionPath) {
|
|
|
51885
52947
|
async function archivedVariantNumbers(root, internalId) {
|
|
51886
52948
|
if (!internalId) return [];
|
|
51887
52949
|
try {
|
|
51888
|
-
const entries = await
|
|
52950
|
+
const entries = await readdir13(path41.resolve(root, "src/_variants", internalId), { withFileTypes: true });
|
|
51889
52951
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => Number(entry.name)).filter((n) => Number.isInteger(n) && n > 0);
|
|
51890
52952
|
} catch {
|
|
51891
52953
|
return [];
|
|
@@ -51900,14 +52962,14 @@ function fail7(code, message, fix) {
|
|
|
51900
52962
|
}
|
|
51901
52963
|
async function isDir3(p) {
|
|
51902
52964
|
try {
|
|
51903
|
-
return (await
|
|
52965
|
+
return (await stat9(p)).isDirectory();
|
|
51904
52966
|
} catch {
|
|
51905
52967
|
return false;
|
|
51906
52968
|
}
|
|
51907
52969
|
}
|
|
51908
52970
|
async function exists(p) {
|
|
51909
52971
|
try {
|
|
51910
|
-
await
|
|
52972
|
+
await stat9(p);
|
|
51911
52973
|
return true;
|
|
51912
52974
|
} catch {
|
|
51913
52975
|
return false;
|
|
@@ -51915,7 +52977,7 @@ async function exists(p) {
|
|
|
51915
52977
|
}
|
|
51916
52978
|
async function listLandingSlugs2(root) {
|
|
51917
52979
|
try {
|
|
51918
|
-
const entries = await
|
|
52980
|
+
const entries = await readdir13(path41.resolve(root, "src", "pages"), { withFileTypes: true });
|
|
51919
52981
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
51920
52982
|
} catch {
|
|
51921
52983
|
return [];
|
|
@@ -51924,14 +52986,14 @@ async function listLandingSlugs2(root) {
|
|
|
51924
52986
|
async function listComponents(componentsDir, prefix = "") {
|
|
51925
52987
|
let entries;
|
|
51926
52988
|
try {
|
|
51927
|
-
entries = await
|
|
52989
|
+
entries = await readdir13(componentsDir, { withFileTypes: true });
|
|
51928
52990
|
} catch {
|
|
51929
52991
|
return [];
|
|
51930
52992
|
}
|
|
51931
52993
|
const out = [];
|
|
51932
52994
|
for (const entry of entries) {
|
|
51933
52995
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
51934
|
-
if (entry.isDirectory()) out.push(...await listComponents(
|
|
52996
|
+
if (entry.isDirectory()) out.push(...await listComponents(path41.join(componentsDir, entry.name), rel));
|
|
51935
52997
|
else if (entry.name.endsWith(".astro")) out.push(rel);
|
|
51936
52998
|
}
|
|
51937
52999
|
return out.sort();
|
|
@@ -51945,27 +53007,39 @@ function resolveForkName(requested, available) {
|
|
|
51945
53007
|
const matches = available.filter((a) => basename4(a) === cleaned || basename4(a) === `${cleaned}.astro`);
|
|
51946
53008
|
return matches.length === 1 ? matches[0] ?? null : null;
|
|
51947
53009
|
}
|
|
51948
|
-
function parseForks(fork) {
|
|
51949
|
-
const
|
|
51950
|
-
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);
|
|
51951
53012
|
return [...new Set(names)];
|
|
51952
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
|
+
}
|
|
51953
53026
|
function buildVariantDefinition(controlDefinition, opts) {
|
|
51954
|
-
|
|
53027
|
+
const frontmatter = controlDefinition.match(/^---\n[\s\S]*?\n---/)?.[0] ?? "---\n---";
|
|
53028
|
+
let out = frontmatter;
|
|
51955
53029
|
out = out.replace(/^internalId:.*$/m, `internalId: "${opts.internalId}"`);
|
|
51956
53030
|
out = out.replace(/^internalTitle:\s*(.*)$/m, (_m, title) => `internalTitle: ${title.trim()} (variant)`);
|
|
51957
53031
|
const shared = opts.forks.length > 0 ? opts.forks.join(", ") : "nothing yet";
|
|
51958
53032
|
const note = [
|
|
51959
53033
|
"",
|
|
51960
|
-
|
|
53034
|
+
`# Version ${variantNumberOf(opts.variantSlug) ?? "?"} of \`${opts.controlSlug}\` \u2014 for an A/B test`,
|
|
51961
53035
|
"",
|
|
51962
|
-
`This
|
|
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.`,
|
|
51963
53037
|
"",
|
|
51964
|
-
`**Only this differs:** ${shared}. Every other section is imported from \`${opts.controlSlug}\`, on purpose \u2014 an edit to the
|
|
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/\`.`,
|
|
51965
53039
|
...opts.because ? ["", `**Because:** ${opts.because}`] : [],
|
|
51966
53040
|
...opts.change ? ["", `**We changed:** ${opts.change}`] : [],
|
|
51967
53041
|
"",
|
|
51968
|
-
`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.`,
|
|
51969
53043
|
""
|
|
51970
53044
|
].join("\n");
|
|
51971
53045
|
return `${out.trimEnd()}
|
|
@@ -51986,16 +53060,16 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
51986
53060
|
`"${controlSlug}" is already a variant of "${variantOfSlug(controlSlug)}". Make the next variant of the page itself: \`baker landing variant ${variantOfSlug(controlSlug)}\`.`
|
|
51987
53061
|
);
|
|
51988
53062
|
}
|
|
51989
|
-
const controlDir =
|
|
53063
|
+
const controlDir = path41.resolve(projectRoot, "src", "pages", controlSlug);
|
|
51990
53064
|
if (!await isDir3(controlDir)) {
|
|
51991
53065
|
fail7("NOT_FOUND", `No page at src/pages/${controlSlug}/`, {
|
|
51992
53066
|
availableSlugs: await listLandingSlugs2(projectRoot)
|
|
51993
53067
|
});
|
|
51994
53068
|
}
|
|
51995
|
-
if (!await exists(
|
|
53069
|
+
if (!await exists(path41.join(controlDir, "index.astro"))) {
|
|
51996
53070
|
fail7("NOT_FOUND", `src/pages/${controlSlug}/index.astro is missing, so there is no page to make a variant of.`);
|
|
51997
53071
|
}
|
|
51998
|
-
const internalId = await readInternalId(
|
|
53072
|
+
const internalId = await readInternalId(path41.join(controlDir, "_definition.md"));
|
|
51999
53073
|
const variantSlugValue = variantSlug(
|
|
52000
53074
|
controlSlug,
|
|
52001
53075
|
nextVariantNumber(
|
|
@@ -52004,10 +53078,10 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
52004
53078
|
await archivedVariantNumbers(projectRoot, internalId)
|
|
52005
53079
|
)
|
|
52006
53080
|
);
|
|
52007
|
-
const variantDir =
|
|
52008
|
-
const available = await listComponents(
|
|
53081
|
+
const variantDir = path41.resolve(projectRoot, "src", "pages", variantSlugValue);
|
|
53082
|
+
const available = await listComponents(path41.join(controlDir, "_components"));
|
|
52009
53083
|
const forks = [];
|
|
52010
|
-
for (const name of parseForks(opts.fork)) {
|
|
53084
|
+
for (const name of parseForks(opts.fork, opts.rawArgs)) {
|
|
52011
53085
|
const resolved = resolveForkName(name, available);
|
|
52012
53086
|
if (resolved === null) {
|
|
52013
53087
|
fail7("NOT_FOUND", `"${name}" is not a component of ${controlSlug}.`, {
|
|
@@ -52021,26 +53095,26 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
52021
53095
|
}
|
|
52022
53096
|
async function writeVariant(opts) {
|
|
52023
53097
|
const { controlDir, variantDir, controlSlug, variantSlug: variantSlug2, forks } = opts;
|
|
52024
|
-
const forkedTargets = new Set(forks.map((f) =>
|
|
53098
|
+
const forkedTargets = new Set(forks.map((f) => path41.join(controlDir, "_components", f)));
|
|
52025
53099
|
const written = [];
|
|
52026
|
-
const indexText = await
|
|
53100
|
+
const indexText = await readFile27(path41.join(controlDir, "index.astro"), "utf8");
|
|
52027
53101
|
await mkdir13(variantDir, { recursive: true });
|
|
52028
53102
|
await writeFile18(
|
|
52029
|
-
|
|
53103
|
+
path41.join(variantDir, "index.astro"),
|
|
52030
53104
|
repointFile(indexText, { fromDir: controlDir, toDir: variantDir, controlDir, variantDir, forkedTargets }),
|
|
52031
53105
|
"utf8"
|
|
52032
53106
|
);
|
|
52033
53107
|
written.push(`src/pages/${variantSlug2}/index.astro`);
|
|
52034
53108
|
for (const fork of forks) {
|
|
52035
|
-
const fromFile =
|
|
52036
|
-
const toFile =
|
|
52037
|
-
const text2 = await
|
|
52038
|
-
await mkdir13(
|
|
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 });
|
|
52039
53113
|
await writeFile18(
|
|
52040
53114
|
toFile,
|
|
52041
53115
|
repointFile(text2, {
|
|
52042
|
-
fromDir:
|
|
52043
|
-
toDir:
|
|
53116
|
+
fromDir: path41.dirname(fromFile),
|
|
53117
|
+
toDir: path41.dirname(toFile),
|
|
52044
53118
|
controlDir,
|
|
52045
53119
|
variantDir,
|
|
52046
53120
|
forkedTargets
|
|
@@ -52049,9 +53123,9 @@ async function writeVariant(opts) {
|
|
|
52049
53123
|
);
|
|
52050
53124
|
written.push(`src/pages/${variantSlug2}/_components/${fork}`);
|
|
52051
53125
|
}
|
|
52052
|
-
const controlDefinitionPath =
|
|
53126
|
+
const controlDefinitionPath = path41.join(controlDir, "_definition.md");
|
|
52053
53127
|
if (await exists(controlDefinitionPath)) {
|
|
52054
|
-
const definition = buildVariantDefinition(await
|
|
53128
|
+
const definition = buildVariantDefinition(await readFile27(controlDefinitionPath, "utf8"), {
|
|
52055
53129
|
internalId: randomUUID2().replace(/-/g, "").slice(0, 8),
|
|
52056
53130
|
variantSlug: variantSlug2,
|
|
52057
53131
|
controlSlug,
|
|
@@ -52059,11 +53133,11 @@ async function writeVariant(opts) {
|
|
|
52059
53133
|
...opts.because ? { because: opts.because } : {},
|
|
52060
53134
|
...opts.change ? { change: opts.change } : {}
|
|
52061
53135
|
});
|
|
52062
|
-
await writeFile18(
|
|
53136
|
+
await writeFile18(path41.join(variantDir, "_definition.md"), definition, "utf8");
|
|
52063
53137
|
written.push(`src/pages/${variantSlug2}/_definition.md`);
|
|
52064
53138
|
}
|
|
52065
|
-
await mkdir13(
|
|
52066
|
-
await writeFile18(
|
|
53139
|
+
await mkdir13(path41.join(variantDir, "_images"), { recursive: true });
|
|
53140
|
+
await writeFile18(path41.join(variantDir, "_images", ".gitkeep"), "", "utf8");
|
|
52067
53141
|
return written;
|
|
52068
53142
|
}
|
|
52069
53143
|
var variantCommand = defineCommand174({
|
|
@@ -52088,13 +53162,28 @@ var variantCommand = defineCommand174({
|
|
|
52088
53162
|
description: "What is different about this version, in the client's words \u2014 \u201Cput the booking form in the hero\u201D."
|
|
52089
53163
|
}
|
|
52090
53164
|
},
|
|
52091
|
-
async run({ args }) {
|
|
53165
|
+
async run({ args, rawArgs }) {
|
|
52092
53166
|
const projectRoot = process.cwd();
|
|
52093
53167
|
const controlSlug = String(args.page);
|
|
52094
53168
|
const { controlDir, variantDir, variantSlug: variantSlug2, available, forks } = await resolveTargets(projectRoot, {
|
|
52095
53169
|
controlSlug,
|
|
52096
|
-
fork: args.fork
|
|
53170
|
+
fork: args.fork,
|
|
53171
|
+
rawArgs
|
|
52097
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 });
|
|
52098
53187
|
const written = await writeVariant({
|
|
52099
53188
|
controlDir,
|
|
52100
53189
|
variantDir,
|
|
@@ -54070,8 +55159,8 @@ var listCommand16 = defineCommand193({
|
|
|
54070
55159
|
});
|
|
54071
55160
|
|
|
54072
55161
|
// src/commands/scheduled-actions/templates.ts
|
|
54073
|
-
import { readFile as
|
|
54074
|
-
import
|
|
55162
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
55163
|
+
import path42 from "path";
|
|
54075
55164
|
import { defineCommand as defineCommand194 } from "citty";
|
|
54076
55165
|
registerSchema({
|
|
54077
55166
|
command: "scheduled-actions.templates",
|
|
@@ -54174,7 +55263,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
54174
55263
|
}
|
|
54175
55264
|
if (save.length > 0) {
|
|
54176
55265
|
const briefFile = flag("brief-file");
|
|
54177
|
-
const brief = briefFile.length > 0 ? await
|
|
55266
|
+
const brief = briefFile.length > 0 ? await readFile28(path42.resolve(briefFile), "utf8") : flag("brief");
|
|
54178
55267
|
if (brief.trim().length === 0) {
|
|
54179
55268
|
failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
|
|
54180
55269
|
}
|
|
@@ -54295,7 +55384,7 @@ registerSchema({
|
|
|
54295
55384
|
prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
|
|
54296
55385
|
}
|
|
54297
55386
|
});
|
|
54298
|
-
var
|
|
55387
|
+
var updateCommand4 = defineCommand196({
|
|
54299
55388
|
meta: {
|
|
54300
55389
|
name: "update",
|
|
54301
55390
|
description: "Stage a scheduled action update. Examples: baker scheduled-actions update <id> --enabled false | baker scheduled-actions update <id> --mode publish"
|
|
@@ -54394,7 +55483,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
54394
55483
|
list: listCommand16,
|
|
54395
55484
|
get: getCommand4,
|
|
54396
55485
|
create: createCommand3,
|
|
54397
|
-
update:
|
|
55486
|
+
update: updateCommand4,
|
|
54398
55487
|
delete: deleteCommand3,
|
|
54399
55488
|
templates: templatesCommand,
|
|
54400
55489
|
trigger: triggerCommand
|
|
@@ -54645,7 +55734,7 @@ function parseImageRefs(spec) {
|
|
|
54645
55734
|
}
|
|
54646
55735
|
var defaultDeps = {
|
|
54647
55736
|
ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
|
|
54648
|
-
upload: (
|
|
55737
|
+
upload: (path44) => uploadLocalImage({ file: path44, contentType: detectImageContentType(path44), source: "uploaded" })
|
|
54649
55738
|
};
|
|
54650
55739
|
async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
54651
55740
|
const refs = parseImageRefs(spec);
|
|
@@ -54665,10 +55754,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
|
54665
55754
|
}
|
|
54666
55755
|
return { imageIds, added };
|
|
54667
55756
|
}
|
|
54668
|
-
function uploadFailure(
|
|
55757
|
+
function uploadFailure(path44) {
|
|
54669
55758
|
return (error) => {
|
|
54670
55759
|
if (error instanceof ApiError) throw error;
|
|
54671
|
-
throw new ApiError("VALIDATION_ERROR", `Could not read "${
|
|
55760
|
+
throw new ApiError("VALIDATION_ERROR", `Could not read "${path44}" as an image.`);
|
|
54672
55761
|
};
|
|
54673
55762
|
}
|
|
54674
55763
|
|
|
@@ -55666,7 +56755,7 @@ import { defineCommand as defineCommand211 } from "citty";
|
|
|
55666
56755
|
import { defineCommand as defineCommand208 } from "citty";
|
|
55667
56756
|
|
|
55668
56757
|
// src/commands/tag-manager/shared.ts
|
|
55669
|
-
import { readFileSync as
|
|
56758
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
55670
56759
|
function failValidation5(message) {
|
|
55671
56760
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
55672
56761
|
process.exit(1);
|
|
@@ -55682,7 +56771,7 @@ function requireTarget6(args, entity) {
|
|
|
55682
56771
|
function loadJsonArg2(args, flag = "json") {
|
|
55683
56772
|
const inline = args[flag];
|
|
55684
56773
|
const file = args.file;
|
|
55685
|
-
const raw = typeof file === "string" && file.length > 0 ?
|
|
56774
|
+
const raw = typeof file === "string" && file.length > 0 ? readFileSync18(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
|
|
55686
56775
|
if (raw === void 0) {
|
|
55687
56776
|
failValidation5(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
|
|
55688
56777
|
}
|
|
@@ -55730,10 +56819,10 @@ async function stageOp4(op) {
|
|
|
55730
56819
|
handleError5(err);
|
|
55731
56820
|
}
|
|
55732
56821
|
}
|
|
55733
|
-
async function draftAction3(
|
|
56822
|
+
async function draftAction3(path44, body, chat) {
|
|
55734
56823
|
const chatId = resolveChatId(chat);
|
|
55735
56824
|
try {
|
|
55736
|
-
const data = await apiPost(
|
|
56825
|
+
const data = await apiPost(path44, { chatId, ...body });
|
|
55737
56826
|
writeJsonEnvelope({ ok: true, data });
|
|
55738
56827
|
return data;
|
|
55739
56828
|
} catch (err) {
|
|
@@ -56868,9 +57957,9 @@ var groupCommand2 = defineCommand219({
|
|
|
56868
57957
|
});
|
|
56869
57958
|
|
|
56870
57959
|
// src/commands/videos/ingest.ts
|
|
56871
|
-
import { mkdtemp as mkdtemp2, rm as rm8, stat as
|
|
57960
|
+
import { mkdtemp as mkdtemp2, rm as rm8, stat as stat10 } from "fs/promises";
|
|
56872
57961
|
import { tmpdir as tmpdir3 } from "os";
|
|
56873
|
-
import
|
|
57962
|
+
import path43 from "path";
|
|
56874
57963
|
import { defineCommand as defineCommand220 } from "citty";
|
|
56875
57964
|
|
|
56876
57965
|
// src/lib/streamUpload.ts
|
|
@@ -57221,7 +58310,7 @@ function ingestUrl(args) {
|
|
|
57221
58310
|
}
|
|
57222
58311
|
async function downloadThenIngest(args, country) {
|
|
57223
58312
|
const vimeoCookie = captureVimeoCookie();
|
|
57224
|
-
const workDir = await mkdtemp2(
|
|
58313
|
+
const workDir = await mkdtemp2(path43.join(tmpdir3(), "videos-ingest-"));
|
|
57225
58314
|
try {
|
|
57226
58315
|
const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
|
|
57227
58316
|
if (isAudioOnly(probe.info)) {
|
|
@@ -57252,7 +58341,7 @@ async function downloadThenIngest(args, country) {
|
|
|
57252
58341
|
// we allow to fetch it cannot drift apart.
|
|
57253
58342
|
timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
|
|
57254
58343
|
});
|
|
57255
|
-
const stats = await
|
|
58344
|
+
const stats = await stat10(filePath);
|
|
57256
58345
|
if (stats.size > MAX_VIDEO_INGEST_BYTES) {
|
|
57257
58346
|
throw new ApiError(
|
|
57258
58347
|
"VALIDATION_ERROR",
|
|
@@ -57357,7 +58446,7 @@ var searchCommand4 = defineCommand221({
|
|
|
57357
58446
|
var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
57358
58447
|
|
|
57359
58448
|
// src/commands/videos/upload.ts
|
|
57360
|
-
import { readFile as
|
|
58449
|
+
import { readFile as readFile29, stat as stat11 } from "fs/promises";
|
|
57361
58450
|
import { basename as basename3, extname as extname4 } from "path";
|
|
57362
58451
|
import { defineCommand as defineCommand222 } from "citty";
|
|
57363
58452
|
var MIME_MAP = {
|
|
@@ -57441,7 +58530,7 @@ var uploadCommand2 = defineCommand222({
|
|
|
57441
58530
|
const originalFilename = basename3(filePath);
|
|
57442
58531
|
const descriptionContext = args.context;
|
|
57443
58532
|
if (args["dry-run"]) {
|
|
57444
|
-
const fileStats = await
|
|
58533
|
+
const fileStats = await stat11(filePath);
|
|
57445
58534
|
writeJson({
|
|
57446
58535
|
ok: true,
|
|
57447
58536
|
dryRun: true,
|
|
@@ -57454,7 +58543,7 @@ var uploadCommand2 = defineCommand222({
|
|
|
57454
58543
|
originalFilename,
|
|
57455
58544
|
descriptionContext
|
|
57456
58545
|
});
|
|
57457
|
-
const fileBuffer = await
|
|
58546
|
+
const fileBuffer = await readFile29(filePath);
|
|
57458
58547
|
const uploadResponse = await fetch(uploadUrl, {
|
|
57459
58548
|
method: "PUT",
|
|
57460
58549
|
headers: { "Content-Type": contentType },
|
|
@@ -58841,7 +59930,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
|
|
|
58841
59930
|
};
|
|
58842
59931
|
}
|
|
58843
59932
|
function commandPathOf(root, argv) {
|
|
58844
|
-
const
|
|
59933
|
+
const path44 = [];
|
|
58845
59934
|
let command = root;
|
|
58846
59935
|
for (const token of argv) {
|
|
58847
59936
|
if (token === "--" || token.startsWith("-")) {
|
|
@@ -58852,10 +59941,10 @@ function commandPathOf(root, argv) {
|
|
|
58852
59941
|
if (next === void 0 || typeof next !== "object") {
|
|
58853
59942
|
break;
|
|
58854
59943
|
}
|
|
58855
|
-
|
|
59944
|
+
path44.push(token);
|
|
58856
59945
|
command = next;
|
|
58857
59946
|
}
|
|
58858
|
-
return
|
|
59947
|
+
return path44.join(" ");
|
|
58859
59948
|
}
|
|
58860
59949
|
function refuseUnknownFlags(root, argv) {
|
|
58861
59950
|
const unknown = findUnknownFlags(root, argv);
|
|
@@ -58873,7 +59962,7 @@ function refuseUnknownFlags(root, argv) {
|
|
|
58873
59962
|
}
|
|
58874
59963
|
|
|
58875
59964
|
// src/version.ts
|
|
58876
|
-
import { readFileSync as
|
|
59965
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
58877
59966
|
function packageJsonUrl() {
|
|
58878
59967
|
return new URL("../package.json", import.meta.url);
|
|
58879
59968
|
}
|
|
@@ -58885,7 +59974,7 @@ function parsePackageVersion(raw) {
|
|
|
58885
59974
|
throw new Error("Invalid CLI package.json: missing version");
|
|
58886
59975
|
}
|
|
58887
59976
|
function getCliVersion() {
|
|
58888
|
-
return parsePackageVersion(
|
|
59977
|
+
return parsePackageVersion(readFileSync19(packageJsonUrl(), "utf8"));
|
|
58889
59978
|
}
|
|
58890
59979
|
|
|
58891
59980
|
// src/cli.ts
|
|
@@ -58924,7 +60013,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
|
|
|
58924
60013
|
tags: tagsCommand4,
|
|
58925
60014
|
"tag-manager": tagManagerCommand,
|
|
58926
60015
|
chats: chatsCommand,
|
|
58927
|
-
history:
|
|
60016
|
+
history: historyCommand2,
|
|
58928
60017
|
hubspot: hubspotCommand,
|
|
58929
60018
|
"winning-ads": winningAdsCommand,
|
|
58930
60019
|
mcp: mcpCommand,
|