@koda-sl/baker-cli 0.287.0-dev.93be96120 → 0.290.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -10
- package/dist/{chunk-LN5O53BG.js → chunk-NNQDWCFG.js} +60 -14
- package/dist/chunk-NNQDWCFG.js.map +1 -0
- package/dist/cli.js +1605 -539
- 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-NNQDWCFG.js";
|
|
62
62
|
import {
|
|
63
63
|
csvOrJson,
|
|
64
64
|
daysAgoIso,
|
|
@@ -10509,14 +10509,14 @@ function collectionLine(collection) {
|
|
|
10509
10509
|
}
|
|
10510
10510
|
function accountsInTree(nodes) {
|
|
10511
10511
|
const accounts = /* @__PURE__ */ new Set();
|
|
10512
|
-
const
|
|
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,7 +39387,7 @@ 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
|
};
|
|
@@ -38801,7 +39398,7 @@ var LIVE_HINT = {
|
|
|
38801
39398
|
keep_running: (row) => `${row.experimentId} cannot yet tell the two pages apart \u2014 do not read its numbers as a result, and do not end it hoping for one. ${describeWait(row)}`,
|
|
38802
39399
|
winner: (row) => row.winner === "variant" ? `${row.experimentId} has a winner: the new variant. Run \`baker experiment finish --id ${row.experimentId} --keep variant\` to show it to everyone, then \`baker experiment fold\`.` : `${row.experimentId} has a winner: the page as it is. Run \`baker experiment finish --id ${row.experimentId} --keep original\` to put all the traffic back on it.`,
|
|
38803
39400
|
no_difference: (row) => `${row.experimentId} measured the two pages as alike \u2014 it collected enough traffic to see the change it was looking for, and there was none. End it with \`--keep original\` and test a bigger change \u2014 a headline, an offer, the shape of the page, not a button colour.`,
|
|
38804
|
-
stopped_early_harmful: (row) => `${row.experimentId} is doing damage and is STILL being shown to
|
|
39401
|
+
stopped_early_harmful: (row) => `${row.experimentId} is doing damage and is STILL being shown to visitors. Run \`baker experiment finish --id ${row.experimentId} --keep original\` now.`,
|
|
38805
39402
|
// Unreachable on a live test — only ending one produces it. The Record is
|
|
38806
39403
|
// exhaustive by type so a verdict added upstream fails to compile here rather
|
|
38807
39404
|
// than printing nothing.
|
|
@@ -38820,8 +39417,18 @@ function describeWait(row) {
|
|
|
38820
39417
|
function pausedNote(row) {
|
|
38821
39418
|
return row.status === "paused" ? `${row.experimentId} is paused \u2014 everyone sees the original. \`baker experiment resume --id ${row.experimentId}\` restarts the split with the same sides. ` : "";
|
|
38822
39419
|
}
|
|
39420
|
+
function unlearnedHint(experiments) {
|
|
39421
|
+
const unlearned = experiments.filter(
|
|
39422
|
+
(row) => row.status === "finished" && row.learning === null && row.verdict !== "inconclusive"
|
|
39423
|
+
);
|
|
39424
|
+
if (unlearned.length === 0) return null;
|
|
39425
|
+
const ids = unlearned.map((row) => row.experimentId).join(", ");
|
|
39426
|
+
return `${unlearned.length === 1 ? "One finished test has" : `${unlearned.length} finished tests have`} no learning written down yet (${ids}). Say what it taught in one or two sentences \u2014 what the client should believe about this page now \u2014 with \`baker experiment update --id <id> --learning "\u2026"\`. It is what \`plan\` hands the next test.`;
|
|
39427
|
+
}
|
|
38823
39428
|
function buildStatusHints(experiments) {
|
|
38824
39429
|
const hints2 = experiments.filter((row) => row.status !== "finished").map((row) => pausedNote(row) + LIVE_HINT[row.verdict](row));
|
|
39430
|
+
const unlearned = unlearnedHint(experiments);
|
|
39431
|
+
if (unlearned) hints2.push(unlearned);
|
|
38825
39432
|
if (experiments.length === 0) {
|
|
38826
39433
|
hints2.push(
|
|
38827
39434
|
"No tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page has enough traffic for one before you build the alternative."
|
|
@@ -38842,24 +39449,50 @@ function horizonsHint(plan) {
|
|
|
38842
39449
|
}
|
|
38843
39450
|
function conversionsHint(plan) {
|
|
38844
39451
|
if (plan.conversions.length <= 1) return null;
|
|
38845
|
-
const others = plan.conversions.filter((entry) => entry.
|
|
38846
|
-
return `This page's visitors also went on to: ${others.map((entry) =>
|
|
39452
|
+
const others = plan.conversions.filter((entry) => entry.eventKey !== plan.goal);
|
|
39453
|
+
return `This page's visitors also went on to: ${others.map((entry) => `\`${entry.eventKey}\`${entry.name ? ` (\u201C${entry.name}\u201D)` : ""} \u2014 ${entry.convertingVisitors}`).join(
|
|
39454
|
+
", "
|
|
39455
|
+
)}. Any of them can be the goal (--goal <key>); the named ones are reported beside it on every result, and decide nothing.`;
|
|
39456
|
+
}
|
|
39457
|
+
function pastTestLine(test) {
|
|
39458
|
+
const outcome = {
|
|
39459
|
+
supported: "the idea held",
|
|
39460
|
+
refuted_opposite: "the idea was WRONG \u2014 the change made it worse",
|
|
39461
|
+
refuted_no_effect: "no difference \u2014 the change did not matter",
|
|
39462
|
+
unanswered: "never answered"
|
|
39463
|
+
};
|
|
39464
|
+
const kept = test.survivor === "variant" ? "the new version was kept" : "the original was kept";
|
|
39465
|
+
const learning = test.learning ? ` Learning: ${test.learning}` : "";
|
|
39466
|
+
return `\u201C${test.hypothesis.change}\u201D (${test.experimentId}, ${test.ranForDays} days): ${outcome[test.hypothesisOutcome]}; ${kept}${test.againstVerdict ? " against the numbers" : ""}.${learning}`;
|
|
39467
|
+
}
|
|
39468
|
+
function pastTestsHint(plan) {
|
|
39469
|
+
if (plan.pastTests.length === 0) return null;
|
|
39470
|
+
const lines = plan.pastTests.slice(0, 5).map(pastTestLine);
|
|
39471
|
+
const more = plan.pastTests.length > 5 ? ` \u2026and ${plan.pastTests.length - 5} more \u2014 \`baker experiment history --landing <slug>\` lists them all.` : "";
|
|
39472
|
+
return `This page has been tested before \u2014 read this before choosing what to change, and build on it rather than repeating it: ${lines.join(" ")}${more}`;
|
|
38847
39473
|
}
|
|
38848
39474
|
function buildPlanHints(plan) {
|
|
38849
39475
|
const horizons = horizonsHint(plan);
|
|
39476
|
+
const past = pastTestsHint(plan);
|
|
38850
39477
|
if (!plan.canRun) {
|
|
38851
39478
|
return [
|
|
38852
39479
|
plan.reason ?? "This test cannot run on this page.",
|
|
38853
39480
|
...horizons && plan.visitors > 0 ? [horizons] : [],
|
|
39481
|
+
...past ? [past] : [],
|
|
38854
39482
|
"A test that cannot conclude is worse than no test \u2014 it still produces a number, and somebody acts on it."
|
|
38855
39483
|
];
|
|
38856
39484
|
}
|
|
38857
39485
|
const hints2 = [
|
|
39486
|
+
...past ? [past] : [],
|
|
39487
|
+
// First, because it changes how every number below should be read: a plan
|
|
39488
|
+
// sized on a stand-in is a guide, and the agent must say so to the client
|
|
39489
|
+
// rather than quote "about 70 days" as though it were measured.
|
|
39490
|
+
...plan.warning ? [plan.warning] : [],
|
|
38858
39491
|
`This test is read on \u201C${plan.goal}\u201D. That is fixed when it starts and cannot be changed later.`,
|
|
38859
39492
|
// Fires at the exact moment the hypothesis gets invented, whether or not
|
|
38860
39493
|
// the agent read the family doc. `plan` says a question CAN be settled here
|
|
38861
39494
|
// and says nothing about which question is worth asking — and a well-run
|
|
38862
|
-
// test of a bad idea
|
|
39495
|
+
// test of a bad idea spends the page's traffic and returns `no_difference`.
|
|
38863
39496
|
"Now decide WHAT to change, from evidence rather than taste. These numbers say which page and which step; they cannot say why. A second opinion answers why \u2014 Microsoft Clarity for what visitors actually do on the page, `baker gsc` for what they searched, `baker ads <platform>` for what the ad promised, `baker ga4` for the client's own measurement. `baker capabilities` lists everything this client has in one call. Message match \u2014 what the ad or the search promised against what the page opens with \u2014 is the cheapest hypothesis worth testing and is wrong surprisingly often.",
|
|
38864
39497
|
// The base always works, so a missing integration is never a reason to
|
|
38865
39498
|
// stop. Naming what it would have shown is worth more to the client than
|
|
@@ -38870,12 +39503,60 @@ function buildPlanHints(plan) {
|
|
|
38870
39503
|
if (conversions) hints2.push(conversions);
|
|
38871
39504
|
if (plan.estimatedDays !== null && plan.estimatedDays > 60) {
|
|
38872
39505
|
hints2.push(
|
|
38873
|
-
`At this page's traffic it would take about ${plan.estimatedDays} days.
|
|
39506
|
+
`At this page's recent traffic it would take about ${plan.estimatedDays} days \u2014 a guide, not a limit: it ends when the numbers are clear, and more traffic (a campaign, ads) shortens it. If the traffic will stay as it is, test a bigger change, which needs fewer visitors to detect.`
|
|
38874
39507
|
);
|
|
38875
39508
|
}
|
|
38876
39509
|
if (horizons) hints2.push(horizons);
|
|
38877
39510
|
return hints2;
|
|
38878
39511
|
}
|
|
39512
|
+
function buildHistoryHints(tests, landing) {
|
|
39513
|
+
if (tests.length === 0) {
|
|
39514
|
+
return [
|
|
39515
|
+
landing ? `No finished test on \u201C${landing}\u201D yet \u2014 nothing to build on, and nothing to avoid repeating. \`baker experiment plan\` says whether the page can settle one.` : "No finished tests yet. `baker experiment plan --landing <slug> --variant <slug>` says whether a page can settle one."
|
|
39516
|
+
];
|
|
39517
|
+
}
|
|
39518
|
+
const hints2 = [];
|
|
39519
|
+
const supported = tests.filter((test) => test.hypothesisOutcome === "supported");
|
|
39520
|
+
const refuted = tests.filter((test) => test.hypothesisOutcome === "refuted_opposite");
|
|
39521
|
+
const noEffect = tests.filter((test) => test.hypothesisOutcome === "refuted_no_effect");
|
|
39522
|
+
if (supported.length > 0) {
|
|
39523
|
+
hints2.push(
|
|
39524
|
+
`Held: ${supported.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Build on these \u2014 the same insight usually has more in it.`
|
|
39525
|
+
);
|
|
39526
|
+
}
|
|
39527
|
+
if (refuted.length > 0) {
|
|
39528
|
+
hints2.push(
|
|
39529
|
+
`Made it worse: ${refuted.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. Do not propose these again as they were; the observation behind each may still be right, the change was not.`
|
|
39530
|
+
);
|
|
39531
|
+
}
|
|
39532
|
+
if (noEffect.length > 0) {
|
|
39533
|
+
hints2.push(
|
|
39534
|
+
`Did not matter: ${noEffect.map((test) => `\u201C${test.hypothesis.change}\u201D on ${test.landingSlug}`).join("; ")}. The next change on these pages has to be bigger \u2014 a headline, an offer, the shape of the page.`
|
|
39535
|
+
);
|
|
39536
|
+
}
|
|
39537
|
+
const unlearned = tests.filter((test) => test.learning === null && test.verdict !== "inconclusive");
|
|
39538
|
+
if (unlearned.length > 0) {
|
|
39539
|
+
hints2.push(
|
|
39540
|
+
`${unlearned.length === 1 ? "One test has" : `${unlearned.length} tests have`} no learning written down (${unlearned.map((test) => test.experimentId).join(", ")}). Read its numbers and write one: \`baker experiment update --id <id> --learning "\u2026"\`.`
|
|
39541
|
+
);
|
|
39542
|
+
}
|
|
39543
|
+
return hints2;
|
|
39544
|
+
}
|
|
39545
|
+
function buildStartHints(staged) {
|
|
39546
|
+
const hints2 = [];
|
|
39547
|
+
if (staged.composition && staged.composition.forked.length === 0) {
|
|
39548
|
+
hints2.push(
|
|
39549
|
+
`\u201C${staged.variant}\u201D does not have its own copy of any section \u2014 it renders exactly the same page as \u201C${staged.landing}\u201D, so this test cannot find anything. Fork the section you want to test (\`baker landing variant ${staged.landing} --fork Hero.astro\`) and edit only that file.`
|
|
39550
|
+
);
|
|
39551
|
+
}
|
|
39552
|
+
if (staged.newForms.length > 0 && staged.variantGoal === void 0) {
|
|
39553
|
+
const forms = staged.newForms.map((flow) => `\u201C${flow}\u201D`).join(", ");
|
|
39554
|
+
hints2.push(
|
|
39555
|
+
`\u201C${staged.variant}\u201D uses ${staged.newForms.length === 1 ? "a Form" : "Forms"} the page does not: ${forms}. The test reads the new version on \`${staged.goal}\`, the page's event \u2014 if the new version converts by reaching this Form's ending instead, it can never convert on that key and the test will call it harmful. Say its own event: \`baker experiment update --id <id> --variant-goal submit:${staged.newForms[0]}\` (or the confirmation step's \`form:${staged.newForms[0]}:<node>:<trigger>\` key, from \`baker analytics conversions --candidates\` once the Form has been opened in the preview).`
|
|
39556
|
+
);
|
|
39557
|
+
}
|
|
39558
|
+
return hints2;
|
|
39559
|
+
}
|
|
38879
39560
|
|
|
38880
39561
|
// ../api/src/experiments/composition.ts
|
|
38881
39562
|
import { z as z35 } from "zod";
|
|
@@ -38888,7 +39569,8 @@ var experimentCompositionSchema = z35.object({
|
|
|
38888
39569
|
|
|
38889
39570
|
// ../api/src/experiments/goal.ts
|
|
38890
39571
|
import { z as z36 } from "zod";
|
|
38891
|
-
var
|
|
39572
|
+
var GOAL_KEY_HELP = "A goal is one event, by its key \u2014 `form:<flow>:<node>:<trigger>`, `submit:<flow>`, `page:<name>` or `exit:<host>`. `baker analytics conversions --candidates` lists the ones these pages produce";
|
|
39573
|
+
var experimentGoalSchema = z36.string().trim().min(1).max(400).refine((key) => parseEventKey(key) !== null, { message: GOAL_KEY_HELP });
|
|
38892
39574
|
|
|
38893
39575
|
// ../api/src/experiments/hypothesis.ts
|
|
38894
39576
|
import { z as z37 } from "zod";
|
|
@@ -38943,9 +39625,13 @@ var experimentHypothesisOutcomeSchema = z37.enum([
|
|
|
38943
39625
|
// ../api/src/experiments/wire.ts
|
|
38944
39626
|
import { z as z38 } from "zod";
|
|
38945
39627
|
var slugSchema = z38.string().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a page slug, like `oferta`");
|
|
39628
|
+
var variantSlugSchema = z38.string().min(1).max(130).refine((slug) => {
|
|
39629
|
+
const page = variantOfSlug(slug);
|
|
39630
|
+
return page !== null && slugSchema.safeParse(page).success;
|
|
39631
|
+
}, "Use the variant's slug, `<page>--<n>` \u2014 `baker landing variant <page>` chooses it");
|
|
38946
39632
|
var experimentPlanRequestSchema = z38.object({
|
|
38947
39633
|
landingSlug: slugSchema,
|
|
38948
|
-
variantSlug:
|
|
39634
|
+
variantSlug: variantSlugSchema,
|
|
38949
39635
|
/**
|
|
38950
39636
|
* Which of the company's conversions the test is about.
|
|
38951
39637
|
*
|
|
@@ -38955,6 +39641,15 @@ var experimentPlanRequestSchema = z38.object({
|
|
|
38955
39641
|
* is measuring two things and deciding on their sum.
|
|
38956
39642
|
*/
|
|
38957
39643
|
goal: experimentGoalSchema.optional(),
|
|
39644
|
+
/**
|
|
39645
|
+
* The event the NEW version converts on, when it is not the page's.
|
|
39646
|
+
*
|
|
39647
|
+
* The test asks one question — did more visitors do the thing we want — but
|
|
39648
|
+
* the thing can be a different event on each version: a variant that swaps
|
|
39649
|
+
* the page's Form for a shorter one converts on the new Form's ending. Absent,
|
|
39650
|
+
* the variant is read on `goal`, which is the common case.
|
|
39651
|
+
*/
|
|
39652
|
+
variantGoal: experimentGoalSchema.optional(),
|
|
38958
39653
|
/**
|
|
38959
39654
|
* The smallest lift worth detecting, relative to the baseline. `0.2` is +20%.
|
|
38960
39655
|
*
|
|
@@ -38963,10 +39658,40 @@ var experimentPlanRequestSchema = z38.object({
|
|
|
38963
39658
|
* quarter of a million visitors per arm; `plan` will say so rather than
|
|
38964
39659
|
* running it.
|
|
38965
39660
|
*/
|
|
38966
|
-
minDetectableRelativeLift: z38.number().gt(0).max(10).optional()
|
|
39661
|
+
minDetectableRelativeLift: z38.number().gt(0).max(10).optional(),
|
|
39662
|
+
/**
|
|
39663
|
+
* The share of visitors expected to do the goal, as a fraction — `0.05` is
|
|
39664
|
+
* 5% — for a page whose history cannot say.
|
|
39665
|
+
*
|
|
39666
|
+
* A page about to get its first campaign has no rate to size on. Without
|
|
39667
|
+
* this the plan stands on the company-wide rate for the outcome, or on a
|
|
39668
|
+
* conventional 3%; with it, on what the person setting the test up knows.
|
|
39669
|
+
* Either way the brief says which, because the sizing is a guide and never a
|
|
39670
|
+
* finish line — the test runs until the numbers are clear.
|
|
39671
|
+
*/
|
|
39672
|
+
baselineRate: z38.number().gt(0).lt(1).optional()
|
|
39673
|
+
});
|
|
39674
|
+
var experimentTrafficSchema = z38.enum(["none", "low", "enough"]);
|
|
39675
|
+
var experimentBaselineSourceSchema = z38.enum(["page", "company", "assumed"]);
|
|
39676
|
+
var experimentSizedOnSchema = z38.object({
|
|
39677
|
+
visitors: z38.number(),
|
|
39678
|
+
convertingVisitors: z38.number(),
|
|
39679
|
+
windowDays: z38.number()
|
|
38967
39680
|
});
|
|
38968
39681
|
var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
|
|
38969
39682
|
hypothesis: experimentHypothesisSchema,
|
|
39683
|
+
/**
|
|
39684
|
+
* The two pages' own identities, read off their `_definition.md` by the CLI.
|
|
39685
|
+
*
|
|
39686
|
+
* The backend prefers the `landings` rows, but the variant has no row until
|
|
39687
|
+
* the Session that built it is published — which is after the test is
|
|
39688
|
+
* staged. Without this the variant's identity was never recorded, so a
|
|
39689
|
+
* renamed variant folder detached its running test and a fold archived the
|
|
39690
|
+
* loser under the wrong key. Optional because a workspace that cannot read
|
|
39691
|
+
* its own definition must still be able to start a test.
|
|
39692
|
+
*/
|
|
39693
|
+
landingInternalId: z38.string().min(1).max(64).optional(),
|
|
39694
|
+
variantInternalId: z38.string().min(1).max(64).optional(),
|
|
38970
39695
|
/**
|
|
38971
39696
|
* What the two arms are made of, read off the import graph by the caller at
|
|
38972
39697
|
* the moment the test starts.
|
|
@@ -38979,18 +39704,60 @@ var experimentStartRequestSchema = experimentPlanRequestSchema.extend({
|
|
|
38979
39704
|
composition: experimentCompositionSchema.optional()
|
|
38980
39705
|
});
|
|
38981
39706
|
var experimentKeepSchema = z38.enum(["original", "variant"]);
|
|
39707
|
+
var experimentEndReasonSchema = z38.enum(["numbers_clear", "not_settling", "page_changing", "test_flawed"]);
|
|
39708
|
+
var experimentLearningSchema = z38.string().trim().min(1).max(2e3);
|
|
38982
39709
|
var experimentFinishRequestSchema = z38.object({
|
|
38983
39710
|
experimentId: z38.string().min(1).max(64),
|
|
38984
|
-
keep: experimentKeepSchema
|
|
39711
|
+
keep: experimentKeepSchema,
|
|
39712
|
+
/** Why it is being ended. The dashboard always asks; the agent should say. */
|
|
39713
|
+
reason: experimentEndReasonSchema.optional(),
|
|
39714
|
+
learning: experimentLearningSchema.optional()
|
|
38985
39715
|
});
|
|
38986
39716
|
var experimentPauseRequestSchema = z38.object({
|
|
38987
39717
|
experimentId: z38.string().min(1).max(64)
|
|
38988
39718
|
});
|
|
39719
|
+
var experimentUpdateRequestSchema = z38.object({
|
|
39720
|
+
experimentId: z38.string().min(1).max(64),
|
|
39721
|
+
because: experimentHypothesisSchema.shape.because.optional(),
|
|
39722
|
+
change: experimentHypothesisSchema.shape.change.optional(),
|
|
39723
|
+
evidence: experimentHypothesisSchema.shape.evidence,
|
|
39724
|
+
/**
|
|
39725
|
+
* Staged tests only. Spelled out rather than borrowed from the hypothesis
|
|
39726
|
+
* schema, whose `expect` defaults to `increase`: through `.optional()` that
|
|
39727
|
+
* default still fires, so every wording-only update arrived carrying a
|
|
39728
|
+
* direction and was refused for trying to change it.
|
|
39729
|
+
*/
|
|
39730
|
+
expect: z38.enum(["increase", "decrease"]).optional(),
|
|
39731
|
+
/** Staged tests only. */
|
|
39732
|
+
goal: experimentGoalSchema.optional(),
|
|
39733
|
+
/** Staged tests only. The new version's own event, when it differs. */
|
|
39734
|
+
variantGoal: experimentGoalSchema.optional(),
|
|
39735
|
+
/** Staged tests only. */
|
|
39736
|
+
minDetectableRelativeLift: z38.number().gt(0).max(10).optional(),
|
|
39737
|
+
/** Finished tests only: what the test taught, written or corrected after the fact. */
|
|
39738
|
+
learning: experimentLearningSchema.optional()
|
|
39739
|
+
});
|
|
39740
|
+
var experimentUpdateResponseSchema = z38.object({
|
|
39741
|
+
/** True when the test had not been published, so its whole plan was re-sized. */
|
|
39742
|
+
staged: z38.boolean(),
|
|
39743
|
+
hypothesisStatement: z38.string()
|
|
39744
|
+
});
|
|
38989
39745
|
var experimentFoldRequestSchema = z38.object({
|
|
38990
39746
|
/** Which test to fold. Omitted ⇒ every fold this company owes. */
|
|
38991
39747
|
experimentId: z38.string().max(64).optional(),
|
|
38992
39748
|
/** The files are written. Baker drops the promotion once the build lands. */
|
|
38993
|
-
applied: z38.boolean().optional()
|
|
39749
|
+
applied: z38.boolean().optional(),
|
|
39750
|
+
/**
|
|
39751
|
+
* Which of the owed folds were actually written, on an `applied` call.
|
|
39752
|
+
*
|
|
39753
|
+
* Folding is per page and one page failing must not hold back the others, so
|
|
39754
|
+
* a run that folds two of three has to be able to say which two. Without it
|
|
39755
|
+
* the report was "the company", every owed test was stamped as written, and
|
|
39756
|
+
* the one that failed had its promotion dropped when the build landed — so
|
|
39757
|
+
* its page served the version that LOST, with nothing owed and nothing
|
|
39758
|
+
* logged. Omitted ⇒ everything owed, which is the whole-company case.
|
|
39759
|
+
*/
|
|
39760
|
+
experimentIds: z38.array(z38.string().max(64)).max(200).optional()
|
|
38994
39761
|
});
|
|
38995
39762
|
var experimentFoldTargetSchema = z38.object({
|
|
38996
39763
|
experimentId: z38.string(),
|
|
@@ -39010,10 +39777,19 @@ var experimentFoldResponseSchema = z38.object({
|
|
|
39010
39777
|
});
|
|
39011
39778
|
var experimentStatusRequestSchema = z38.object({
|
|
39012
39779
|
experimentId: z38.string().max(64).optional(),
|
|
39780
|
+
/** Only the tests on one page, by its slug. */
|
|
39781
|
+
landingSlug: z38.string().max(200).optional(),
|
|
39013
39782
|
full: z38.boolean().optional()
|
|
39014
39783
|
});
|
|
39784
|
+
var experimentHistoryRequestSchema = z38.object({
|
|
39785
|
+
landingSlug: z38.string().max(200).optional(),
|
|
39786
|
+
limit: z38.number().int().positive().max(100).optional()
|
|
39787
|
+
});
|
|
39015
39788
|
var experimentPageConversionSchema = z38.object({
|
|
39016
|
-
|
|
39789
|
+
/** The event, by key — what `--goal` takes. */
|
|
39790
|
+
eventKey: z38.string(),
|
|
39791
|
+
/** The conversion name the company counts it under, or null when nobody has named it. */
|
|
39792
|
+
name: z38.string().nullable(),
|
|
39017
39793
|
/** Distinct visitors to the page who went on to do this. */
|
|
39018
39794
|
convertingVisitors: z38.number()
|
|
39019
39795
|
});
|
|
@@ -39026,9 +39802,11 @@ var experimentPlanResponseSchema = z38.object({
|
|
|
39026
39802
|
canRun: z38.boolean(),
|
|
39027
39803
|
/** Present when `canRun` is false. Product language, ready to show. */
|
|
39028
39804
|
reason: z38.string().optional(),
|
|
39029
|
-
/** The
|
|
39805
|
+
/** The event the test would be read on. Absent when none could be chosen. */
|
|
39030
39806
|
goal: experimentGoalSchema.optional(),
|
|
39031
|
-
/**
|
|
39807
|
+
/** The new version's own event, when one was given. */
|
|
39808
|
+
variantGoal: experimentGoalSchema.optional(),
|
|
39809
|
+
/** Every event this page's visitors went on to do over the sizing window, most common first. */
|
|
39032
39810
|
conversions: z38.array(experimentPageConversionSchema),
|
|
39033
39811
|
/** Distinct visitors the page had over the sizing window. */
|
|
39034
39812
|
visitors: z38.number(),
|
|
@@ -39040,7 +39818,24 @@ var experimentPlanResponseSchema = z38.object({
|
|
|
39040
39818
|
dailyVisitorsPerVariant: z38.number(),
|
|
39041
39819
|
/** `null` when the page has no traffic to project from. */
|
|
39042
39820
|
estimatedDays: z38.number().nullable(),
|
|
39043
|
-
horizons: z38.array(experimentHorizonSchema)
|
|
39821
|
+
horizons: z38.array(experimentHorizonSchema),
|
|
39822
|
+
traffic: experimentTrafficSchema,
|
|
39823
|
+
baselineSource: experimentBaselineSourceSchema,
|
|
39824
|
+
sizedOn: experimentSizedOnSchema,
|
|
39825
|
+
/**
|
|
39826
|
+
* Why the sizing is a guide rather than a measurement, when it is one.
|
|
39827
|
+
* Product language, ready to show; absent on a page with real traffic that
|
|
39828
|
+
* has produced the goal.
|
|
39829
|
+
*/
|
|
39830
|
+
warning: z38.string().optional(),
|
|
39831
|
+
/**
|
|
39832
|
+
* Every finished test on this page, newest first — see
|
|
39833
|
+
* `experimentHistoryEntrySchema`. On the plan because the plan is the moment
|
|
39834
|
+
* a hypothesis is chosen, and a page's own record is the first thing that
|
|
39835
|
+
* should inform it: a refuted idea proposed again, or a supported one never
|
|
39836
|
+
* built on, is what this exists to prevent.
|
|
39837
|
+
*/
|
|
39838
|
+
pastTests: z38.array(z38.lazy(() => experimentHistoryEntrySchema)).default([])
|
|
39044
39839
|
});
|
|
39045
39840
|
var experimentVerdictSchema = z38.enum([
|
|
39046
39841
|
"keep_running",
|
|
@@ -39073,7 +39868,18 @@ var experimentArmSchema = z38.object({
|
|
|
39073
39868
|
* screens cannot show a client two different "this is your offer page".
|
|
39074
39869
|
* `null` before the first capture, and on a page whose capture failed.
|
|
39075
39870
|
*/
|
|
39076
|
-
previewUrl: z38.string().nullable().optional()
|
|
39871
|
+
previewUrl: z38.string().nullable().optional(),
|
|
39872
|
+
/**
|
|
39873
|
+
* This version as it was when the test started — the test's own capture,
|
|
39874
|
+
* taken off the live site once the split was being served.
|
|
39875
|
+
*
|
|
39876
|
+
* `previewUrl` above is the page as it is *today*, which is right while a
|
|
39877
|
+
* test runs and wrong the day after somebody edits the page, and absent
|
|
39878
|
+
* altogether for a variant that lost and was archived. This is the record;
|
|
39879
|
+
* a card prefers it and says which of the two it is showing.
|
|
39880
|
+
*/
|
|
39881
|
+
snapshotUrl: z38.string().nullable().optional(),
|
|
39882
|
+
snapshotCapturedAt: z38.number().nullable().optional()
|
|
39077
39883
|
});
|
|
39078
39884
|
var experimentMetricSchema = z38.object({
|
|
39079
39885
|
name: z38.string(),
|
|
@@ -39124,8 +39930,12 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39124
39930
|
* is not a hypothesis, "leads should go up by at least 20%" is.
|
|
39125
39931
|
*/
|
|
39126
39932
|
minDetectableRelativeLift: z38.number(),
|
|
39127
|
-
/** The
|
|
39933
|
+
/** The event the verdict is read from on the page, by key. */
|
|
39128
39934
|
goal: experimentGoalSchema,
|
|
39935
|
+
/** The event the new version is read on, when it differs from `goal`. */
|
|
39936
|
+
variantGoal: experimentGoalSchema.nullable(),
|
|
39937
|
+
/** Both, in words a marketer reads — the Form's name, the moment. */
|
|
39938
|
+
goalLabel: z38.string(),
|
|
39129
39939
|
status: experimentStatusValueSchema,
|
|
39130
39940
|
startedAt: z38.number(),
|
|
39131
39941
|
/** Set while paused. The split is off and everyone sees the original. */
|
|
@@ -39141,8 +39951,26 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39141
39951
|
survivor: z38.enum(["control", "variant"]).nullable(),
|
|
39142
39952
|
/** Set only on `invalid`. Why the numbers cannot be read. */
|
|
39143
39953
|
invalidReason: z38.enum(["sample_ratio_mismatch", "crossover"]).nullable(),
|
|
39954
|
+
/**
|
|
39955
|
+
* Where the page's own content stands after the test, once it has ended.
|
|
39956
|
+
*
|
|
39957
|
+
* `owed` — the new variant was kept and is served by a rewrite; the page's
|
|
39958
|
+
* source still says otherwise. `landing` — the files are written and the
|
|
39959
|
+
* next publish carries them. `done` — the page's own content is the version
|
|
39960
|
+
* that was kept, and nothing routes either arm any more. `none` — the
|
|
39961
|
+
* original was kept, so there was never anything to move.
|
|
39962
|
+
*
|
|
39963
|
+
* On the row because two things on the card read it: whether "open this
|
|
39964
|
+
* version" links still show the right file, and whether there is anything
|
|
39965
|
+
* left for a person to do.
|
|
39966
|
+
*/
|
|
39967
|
+
fold: z38.enum(["none", "owed", "landing", "done"]).nullable(),
|
|
39144
39968
|
/** What a person should be told, in one sentence, in product language. */
|
|
39145
39969
|
summary: z38.string(),
|
|
39970
|
+
/** What the test taught, once somebody wrote it down. Null until then, and always null while it runs. */
|
|
39971
|
+
learning: z38.string().nullable(),
|
|
39972
|
+
/** Why it was ended, when whoever ended it said. Null while it runs. */
|
|
39973
|
+
endReason: experimentEndReasonSchema.nullable(),
|
|
39146
39974
|
control: experimentArmSchema,
|
|
39147
39975
|
variant: experimentArmSchema,
|
|
39148
39976
|
/** Every other conversion the company counts, on the same two arms. */
|
|
@@ -39177,6 +40005,13 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39177
40005
|
evidence: z38.object({
|
|
39178
40006
|
relativeLift: z38.number(),
|
|
39179
40007
|
probabilityVariantBetter: z38.number(),
|
|
40008
|
+
/**
|
|
40009
|
+
* How sure the numbers are that the two versions differ at all, 0 to 1.
|
|
40010
|
+
* Anytime-valid — the same construction the verdict is read from, so it
|
|
40011
|
+
* holds however often it is read — and a winner is called exactly when
|
|
40012
|
+
* it crosses 95%. The figure a card leads with.
|
|
40013
|
+
*/
|
|
40014
|
+
confidence: z38.number(),
|
|
39180
40015
|
controlInterval: z38.tuple([z38.number(), z38.number()]),
|
|
39181
40016
|
variantInterval: z38.tuple([z38.number(), z38.number()]),
|
|
39182
40017
|
/** The anytime-valid interval on the relative lift — what the verdict is read from. */
|
|
@@ -39186,6 +40021,47 @@ var experimentStatusRowSchema = z38.object({
|
|
|
39186
40021
|
var experimentStatusResponseSchema = z38.object({
|
|
39187
40022
|
experiments: z38.array(experimentStatusRowSchema)
|
|
39188
40023
|
});
|
|
40024
|
+
var experimentHistoryEntrySchema = z38.object({
|
|
40025
|
+
experimentId: z38.string(),
|
|
40026
|
+
landingSlug: z38.string(),
|
|
40027
|
+
variantSlug: z38.string(),
|
|
40028
|
+
startedAt: z38.number(),
|
|
40029
|
+
finishedAt: z38.number(),
|
|
40030
|
+
/** Days the split ran, whole. */
|
|
40031
|
+
ranForDays: z38.number(),
|
|
40032
|
+
hypothesis: experimentHypothesisSchema,
|
|
40033
|
+
hypothesisStatement: z38.string(),
|
|
40034
|
+
/** The sections the new version owned, as labels — what was actually different. */
|
|
40035
|
+
changed: z38.array(z38.string()),
|
|
40036
|
+
goal: experimentGoalSchema,
|
|
40037
|
+
variantGoal: experimentGoalSchema.nullable(),
|
|
40038
|
+
goalLabel: z38.string(),
|
|
40039
|
+
verdict: experimentVerdictSchema,
|
|
40040
|
+
/** Which version the numbers favoured, on `winner`. */
|
|
40041
|
+
winner: z38.enum(["control", "variant"]).nullable(),
|
|
40042
|
+
/** Which version kept the traffic. */
|
|
40043
|
+
survivor: z38.enum(["control", "variant"]),
|
|
40044
|
+
/** True when what was kept is not what the numbers favoured. */
|
|
40045
|
+
againstVerdict: z38.boolean(),
|
|
40046
|
+
hypothesisOutcome: experimentHypothesisOutcomeSchema,
|
|
40047
|
+
/** The numbers, as they were when the test ended. */
|
|
40048
|
+
result: z38.object({
|
|
40049
|
+
control: z38.object({ visitors: z38.number(), conversions: z38.number(), rate: z38.number() }),
|
|
40050
|
+
variant: z38.object({ visitors: z38.number(), conversions: z38.number(), rate: z38.number() }),
|
|
40051
|
+
relativeLift: z38.number(),
|
|
40052
|
+
liftInterval: z38.tuple([z38.number(), z38.number()]),
|
|
40053
|
+
confidence: z38.number(),
|
|
40054
|
+
secondary: z38.array(experimentMetricSchema)
|
|
40055
|
+
}),
|
|
40056
|
+
summary: z38.string(),
|
|
40057
|
+
/** Why it was ended, when whoever ended it said. */
|
|
40058
|
+
endReason: experimentEndReasonSchema.nullable(),
|
|
40059
|
+
/** What the test taught, or null when nobody has written it down yet. */
|
|
40060
|
+
learning: z38.string().nullable()
|
|
40061
|
+
});
|
|
40062
|
+
var experimentHistoryResponseSchema = z38.object({
|
|
40063
|
+
tests: z38.array(experimentHistoryEntrySchema)
|
|
40064
|
+
});
|
|
39189
40065
|
var experimentFinishResponseSchema = z38.object({
|
|
39190
40066
|
verdict: experimentVerdictSchema,
|
|
39191
40067
|
survivor: z38.enum(["control", "variant"]),
|
|
@@ -39214,9 +40090,8 @@ function parseOne(raw) {
|
|
|
39214
40090
|
}
|
|
39215
40091
|
return href ? { source: source.data, note, href } : { source: source.data, note };
|
|
39216
40092
|
}
|
|
39217
|
-
function parseEvidence(raw) {
|
|
39218
|
-
|
|
39219
|
-
const entries = (Array.isArray(raw) ? raw : [raw]).filter((entry) => entry !== "");
|
|
40093
|
+
function parseEvidence(raw, rawArgs) {
|
|
40094
|
+
const entries = repeatedValues(rawArgs, "evidence", raw).map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
39220
40095
|
if (entries.length === 0) return void 0;
|
|
39221
40096
|
const parsed = [];
|
|
39222
40097
|
for (const entry of entries) {
|
|
@@ -39227,10 +40102,65 @@ function parseEvidence(raw) {
|
|
|
39227
40102
|
return parsed;
|
|
39228
40103
|
}
|
|
39229
40104
|
|
|
40105
|
+
// src/commands/experiment/identity.ts
|
|
40106
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
40107
|
+
import path30 from "path";
|
|
40108
|
+
async function readPageInternalId(root, slug) {
|
|
40109
|
+
try {
|
|
40110
|
+
const text2 = await readFile23(path30.resolve(root, "src", "pages", slug, "_definition.md"), "utf8");
|
|
40111
|
+
return /^internalId:\s*"?([^"\s]+)"?\s*$/m.exec(text2)?.[1];
|
|
40112
|
+
} catch {
|
|
40113
|
+
return void 0;
|
|
40114
|
+
}
|
|
40115
|
+
}
|
|
40116
|
+
|
|
40117
|
+
// src/commands/experiment/update.ts
|
|
40118
|
+
function buildUpdateRequest(args, rawArgs) {
|
|
40119
|
+
const expect = args.expect === void 0 ? void 0 : String(args.expect);
|
|
40120
|
+
if (expect !== void 0 && expect !== "increase" && expect !== "decrease") {
|
|
40121
|
+
return {
|
|
40122
|
+
error: "`--expect` is either `increase` or `decrease` \u2014 which way should the goal move if you are right?"
|
|
40123
|
+
};
|
|
40124
|
+
}
|
|
40125
|
+
const evidence = parseEvidence(args.evidence, rawArgs);
|
|
40126
|
+
if (evidence && "error" in evidence) return evidence;
|
|
40127
|
+
const lift = args.lift === void 0 ? void 0 : Number(args.lift);
|
|
40128
|
+
if (lift !== void 0 && !(lift > 0)) {
|
|
40129
|
+
return {
|
|
40130
|
+
error: "`--lift` is the smallest improvement worth detecting, relative \u2014 0.2 is +20%. It must be above zero."
|
|
40131
|
+
};
|
|
40132
|
+
}
|
|
40133
|
+
const request = {
|
|
40134
|
+
experimentId: String(args.id),
|
|
40135
|
+
...typed("because", args.because),
|
|
40136
|
+
...typed("change", args.change),
|
|
40137
|
+
...typed("goal", args.goal),
|
|
40138
|
+
...typed("variantGoal", args.variantGoal),
|
|
40139
|
+
...typed("learning", args.learning),
|
|
40140
|
+
...expect === void 0 ? {} : { expect },
|
|
40141
|
+
...evidence ? { evidence } : {},
|
|
40142
|
+
...lift === void 0 ? {} : { minDetectableRelativeLift: lift }
|
|
40143
|
+
};
|
|
40144
|
+
if (Object.keys(request).length === 1) {
|
|
40145
|
+
return {
|
|
40146
|
+
error: "Nothing to change. Pass --because, --change or --evidence; on a finished test, --learning; on a test that has not been published yet, also --expect, --goal, --variant-goal or --lift"
|
|
40147
|
+
};
|
|
40148
|
+
}
|
|
40149
|
+
return request;
|
|
40150
|
+
}
|
|
40151
|
+
function typed(key, value) {
|
|
40152
|
+
return value === void 0 ? {} : { [key]: String(value) };
|
|
40153
|
+
}
|
|
40154
|
+
|
|
39230
40155
|
// src/commands/experiment/index.ts
|
|
39231
40156
|
var GOAL_ARG = {
|
|
39232
40157
|
type: "string",
|
|
39233
|
-
description: "
|
|
40158
|
+
description: "The ONE event the test is read on, by its key: `form:<flow>:<node>:<trigger>` (a Form reaching a step), `submit:<flow>` (a Form finished), `page:<name>` (a page event or one the client's systems post), `exit:<host>` (a click out). `baker analytics conversions --candidates` lists the keys these pages produce, and `plan` lists the ones this page's visitors went on to do. Fixed for the life of the test. Optional when exactly one of them is a named conversion; otherwise say which. Every conversion the company has named is reported beside the goal and decides nothing.",
|
|
40159
|
+
required: false
|
|
40160
|
+
};
|
|
40161
|
+
var VARIANT_GOAL_ARG = {
|
|
40162
|
+
type: "string",
|
|
40163
|
+
description: "The event the NEW version is read on, when it is not the page's \u2014 a variant that swaps the Form for a shorter one converts on the new Form's ending (`submit:<new-flow>`, or its confirmation step's `form:` key). Same vocabulary as --goal. Leave it out when both versions convert on the same event, which is the common case.",
|
|
39234
40164
|
required: false
|
|
39235
40165
|
};
|
|
39236
40166
|
var LIFT_ARG = {
|
|
@@ -39258,7 +40188,30 @@ var EVIDENCE_ARG = {
|
|
|
39258
40188
|
description: "Where the observation came from, as `source:what it showed`, optionally `|link`. Repeatable. Sources: baker_analytics, clarity, ads, search_console, research, inspiration, client, hunch. Use `hunch` honestly when there is no data \u2014 a stated guess is worth more than a guess dressed as analytics.",
|
|
39259
40189
|
required: false
|
|
39260
40190
|
};
|
|
40191
|
+
var BASELINE_ARG = {
|
|
40192
|
+
type: "string",
|
|
40193
|
+
description: "The share of visitors you expect to do the goal, as a fraction \u2014 0.05 is 5% \u2014 for a page whose history cannot say: a new page, or one about to get its first campaign. Without it a page with no conversions yet is sized on the company-wide rate for the outcome, or on 3%. The sizing is a guide either way: the test runs until the numbers are clear.",
|
|
40194
|
+
required: false
|
|
40195
|
+
};
|
|
39261
40196
|
var ID_ARG = { type: "string", description: "The test, by its id", required: true };
|
|
40197
|
+
var LEARNING_ARG = {
|
|
40198
|
+
type: "string",
|
|
40199
|
+
description: "What this test taught, in one or two sentences the client should believe about this page from now on \u2014 \u201Cthe price is not the objection, the install date is\u201D. The one part of the record the numbers cannot write, and what `plan` hands the next test on this page. Finished tests only.",
|
|
40200
|
+
required: false
|
|
40201
|
+
};
|
|
40202
|
+
var REASON_ARG = {
|
|
40203
|
+
type: "string",
|
|
40204
|
+
description: "Why the test is being ended, as distinct from what the numbers said: `numbers_clear` (the result was clear enough to act on), `not_settling` (too little traffic or too small a difference \u2014 waiting would not help), `page_changing` (the page has to change for another reason), `test_flawed` (the wrong goal, a broken variant). Always say \u2014 the dashboard asks a person the same question, and the next test on this page is planned differently after each answer.",
|
|
40205
|
+
required: false
|
|
40206
|
+
};
|
|
40207
|
+
var LANDING_FILTER_ARG = {
|
|
40208
|
+
type: "string",
|
|
40209
|
+
description: "Only the tests on one page, by its slug",
|
|
40210
|
+
required: false
|
|
40211
|
+
};
|
|
40212
|
+
function sizingHints(data) {
|
|
40213
|
+
return typeof data.warning === "string" ? [data.warning] : [];
|
|
40214
|
+
}
|
|
39262
40215
|
function fail5(message) {
|
|
39263
40216
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
39264
40217
|
process.exit(1);
|
|
@@ -39272,24 +40225,28 @@ function reportApiError(error) {
|
|
|
39272
40225
|
}
|
|
39273
40226
|
registerSchema({
|
|
39274
40227
|
command: "experiment.plan",
|
|
39275
|
-
description: "Whether a page
|
|
40228
|
+
description: "Whether a page can settle a question, and how long it might take, before anything is built",
|
|
39276
40229
|
args: {
|
|
39277
40230
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39278
40231
|
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39279
40232
|
goal: GOAL_ARG,
|
|
39280
|
-
|
|
40233
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40234
|
+
lift: LIFT_ARG,
|
|
40235
|
+
baseline: BASELINE_ARG
|
|
39281
40236
|
}
|
|
39282
40237
|
});
|
|
39283
40238
|
var planCommand = defineCommand119({
|
|
39284
40239
|
meta: {
|
|
39285
40240
|
name: "plan",
|
|
39286
|
-
description: "Start here, BEFORE building the alternative page. Says
|
|
40241
|
+
description: "Start here, BEFORE building the alternative page. Says which event the test would be read on, which events the page's visitors actually go on to do (by key, with the conversion name each is counted under), how long it might take at this page's traffic, and what a 14, 28, 56 or 90-day window could settle. The duration is a guide, never a finish line \u2014 a test ends when the numbers are clear, and a page with no traffic yet can still be tested (`warning` says what it was sized on). What IS refused is a test that could not be measured: a goal this page has demonstrably never produced."
|
|
39287
40242
|
},
|
|
39288
40243
|
args: {
|
|
39289
40244
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
39290
40245
|
variant: { type: "string", description: "The new variant's slug, `<page>--<n>`", required: true },
|
|
39291
40246
|
goal: GOAL_ARG,
|
|
39292
|
-
|
|
40247
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40248
|
+
lift: LIFT_ARG,
|
|
40249
|
+
baseline: BASELINE_ARG
|
|
39293
40250
|
},
|
|
39294
40251
|
run: async ({ args }) => {
|
|
39295
40252
|
try {
|
|
@@ -39297,7 +40254,9 @@ var planCommand = defineCommand119({
|
|
|
39297
40254
|
landingSlug: String(args.landing),
|
|
39298
40255
|
variantSlug: String(args.variant),
|
|
39299
40256
|
...args.goal ? { goal: String(args.goal) } : {},
|
|
39300
|
-
...args.
|
|
40257
|
+
...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
|
|
40258
|
+
...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
|
|
40259
|
+
...args.baseline ? { baselineRate: Number(args.baseline) } : {}
|
|
39301
40260
|
});
|
|
39302
40261
|
writeJsonEnvelope({ ok: true, data: response.data, hints: buildPlanHints(response.data) });
|
|
39303
40262
|
} catch (error) {
|
|
@@ -39316,13 +40275,15 @@ registerSchema({
|
|
|
39316
40275
|
expect: EXPECT_ARG,
|
|
39317
40276
|
evidence: EVIDENCE_ARG,
|
|
39318
40277
|
goal: GOAL_ARG,
|
|
39319
|
-
|
|
40278
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40279
|
+
lift: LIFT_ARG,
|
|
40280
|
+
baseline: BASELINE_ARG
|
|
39320
40281
|
}
|
|
39321
40282
|
});
|
|
39322
40283
|
var startCommand = defineCommand119({
|
|
39323
40284
|
meta: {
|
|
39324
40285
|
name: "start",
|
|
39325
|
-
description: "Stage a test between the page and its new variant. Visitors are split
|
|
40286
|
+
description: "Stage a test between the page and its new variant. Visitors are split between the two versions at the edge, so there is no flicker and nothing for an ad blocker to suppress. Nothing goes live until the session is published \u2014 the person publishing is the review of the new variant. Re-runs `plan` first and refuses on the same grounds."
|
|
39326
40287
|
},
|
|
39327
40288
|
args: {
|
|
39328
40289
|
landing: { type: "string", description: "The page under test, by slug", required: true },
|
|
@@ -39332,20 +40293,33 @@ var startCommand = defineCommand119({
|
|
|
39332
40293
|
expect: EXPECT_ARG,
|
|
39333
40294
|
evidence: EVIDENCE_ARG,
|
|
39334
40295
|
goal: GOAL_ARG,
|
|
39335
|
-
|
|
40296
|
+
variantGoal: VARIANT_GOAL_ARG,
|
|
40297
|
+
lift: LIFT_ARG,
|
|
40298
|
+
baseline: BASELINE_ARG
|
|
39336
40299
|
},
|
|
39337
|
-
run: async ({ args }) => {
|
|
40300
|
+
run: async ({ args, rawArgs }) => {
|
|
39338
40301
|
const expect = args.expect === void 0 ? "increase" : String(args.expect);
|
|
39339
40302
|
if (expect !== "increase" && expect !== "decrease") {
|
|
39340
40303
|
fail5("`--expect` is either `increase` or `decrease` \u2014 which way should the goal move if you are right?");
|
|
39341
40304
|
}
|
|
39342
|
-
const evidence = parseEvidence(args.evidence);
|
|
40305
|
+
const evidence = parseEvidence(args.evidence, rawArgs);
|
|
39343
40306
|
if (evidence && "error" in evidence) fail5(evidence.error);
|
|
39344
40307
|
const composition = await readComposition(process.cwd(), String(args.landing), String(args.variant));
|
|
40308
|
+
const [controlForms, variantForms] = await Promise.all([
|
|
40309
|
+
readForms(process.cwd(), String(args.landing)),
|
|
40310
|
+
readForms(process.cwd(), String(args.variant))
|
|
40311
|
+
]);
|
|
40312
|
+
const newForms = formsOnlyIn(variantForms, controlForms);
|
|
40313
|
+
const [landingInternalId, variantInternalId] = await Promise.all([
|
|
40314
|
+
readPageInternalId(process.cwd(), String(args.landing)),
|
|
40315
|
+
readPageInternalId(process.cwd(), String(args.variant))
|
|
40316
|
+
]);
|
|
39345
40317
|
try {
|
|
39346
40318
|
const response = await apiPost("/api/experiments/start", {
|
|
39347
40319
|
landingSlug: String(args.landing),
|
|
39348
40320
|
variantSlug: String(args.variant),
|
|
40321
|
+
...landingInternalId ? { landingInternalId } : {},
|
|
40322
|
+
...variantInternalId ? { variantInternalId } : {},
|
|
39349
40323
|
hypothesis: {
|
|
39350
40324
|
because: String(args.because),
|
|
39351
40325
|
change: String(args.change),
|
|
@@ -39354,18 +40328,23 @@ var startCommand = defineCommand119({
|
|
|
39354
40328
|
},
|
|
39355
40329
|
...composition ? { composition } : {},
|
|
39356
40330
|
...args.goal ? { goal: String(args.goal) } : {},
|
|
39357
|
-
...args.
|
|
40331
|
+
...args.variantGoal ? { variantGoal: String(args.variantGoal) } : {},
|
|
40332
|
+
...args.lift ? { minDetectableRelativeLift: Number(args.lift) } : {},
|
|
40333
|
+
...args.baseline ? { baselineRate: Number(args.baseline) } : {}
|
|
39358
40334
|
});
|
|
39359
40335
|
writeJsonEnvelope({
|
|
39360
40336
|
ok: true,
|
|
39361
40337
|
data: response.data,
|
|
39362
40338
|
hints: [
|
|
39363
|
-
|
|
39364
|
-
|
|
39365
|
-
|
|
39366
|
-
|
|
39367
|
-
|
|
39368
|
-
|
|
40339
|
+
...sizingHints(response.data),
|
|
40340
|
+
...buildStartHints({
|
|
40341
|
+
landing: String(args.landing),
|
|
40342
|
+
variant: String(args.variant),
|
|
40343
|
+
goal: String(response.data.goal),
|
|
40344
|
+
variantGoal: args.variantGoal === void 0 ? void 0 : String(args.variantGoal),
|
|
40345
|
+
composition,
|
|
40346
|
+
newForms
|
|
40347
|
+
}),
|
|
39369
40348
|
"Staged. The split starts when this session is published \u2014 publishing is the review of the new variant.",
|
|
39370
40349
|
"Check it as often as you like \u2014 `baker experiment status` is safe to read at any moment and holds its error rate however often you ask. What is never safe is reading the numbers instead of the verdict: `keep_running` means this test cannot yet tell the two pages apart, whatever the rates happen to say today."
|
|
39371
40350
|
]
|
|
@@ -39380,6 +40359,7 @@ registerSchema({
|
|
|
39380
40359
|
description: "The verdict on every A/B test, running, paused and finished",
|
|
39381
40360
|
args: {
|
|
39382
40361
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
40362
|
+
landing: LANDING_FILTER_ARG,
|
|
39383
40363
|
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
39384
40364
|
}
|
|
39385
40365
|
});
|
|
@@ -39390,12 +40370,14 @@ var statusCommand6 = defineCommand119({
|
|
|
39390
40370
|
},
|
|
39391
40371
|
args: {
|
|
39392
40372
|
id: { type: "string", description: "One test, by its id", required: false },
|
|
40373
|
+
landing: LANDING_FILTER_ARG,
|
|
39393
40374
|
full: { type: "boolean", description: "Include the posteriors behind the verdict", required: false }
|
|
39394
40375
|
},
|
|
39395
40376
|
run: async ({ args }) => {
|
|
39396
40377
|
try {
|
|
39397
40378
|
const response = await apiPost("/api/experiments/status", {
|
|
39398
40379
|
...args.id ? { experimentId: String(args.id) } : {},
|
|
40380
|
+
...args.landing ? { landingSlug: String(args.landing) } : {},
|
|
39399
40381
|
...args.full === true ? { full: true } : {}
|
|
39400
40382
|
});
|
|
39401
40383
|
writeJsonEnvelope({
|
|
@@ -39408,6 +40390,41 @@ var statusCommand6 = defineCommand119({
|
|
|
39408
40390
|
}
|
|
39409
40391
|
}
|
|
39410
40392
|
});
|
|
40393
|
+
registerSchema({
|
|
40394
|
+
command: "experiment.history",
|
|
40395
|
+
description: "Every finished A/B test with what it found and what it taught \u2014 the record to read before proposing the next one",
|
|
40396
|
+
args: {
|
|
40397
|
+
landing: LANDING_FILTER_ARG,
|
|
40398
|
+
limit: { type: "string", description: "At most this many, newest first (default all)", required: false }
|
|
40399
|
+
}
|
|
40400
|
+
});
|
|
40401
|
+
var historyCommand = defineCommand119({
|
|
40402
|
+
meta: {
|
|
40403
|
+
name: "history",
|
|
40404
|
+
description: "What was tested before and what it taught. Finished tests only, newest first: the claim, what was different, what the numbers said, what was kept and whether that went against them, and the `learning` somebody wrote down. Read it \u2014 `--landing <slug>` for one page \u2014 BEFORE proposing a test: a refuted idea proposed again spends the page's traffic to return the same answer, and a supported one is what the next test should build on. `plan` hands the same entries back as `pastTests` for the page it sizes."
|
|
40405
|
+
},
|
|
40406
|
+
args: {
|
|
40407
|
+
landing: LANDING_FILTER_ARG,
|
|
40408
|
+
limit: { type: "string", description: "At most this many, newest first", required: false }
|
|
40409
|
+
},
|
|
40410
|
+
run: async ({ args }) => {
|
|
40411
|
+
const limit = args.limit === void 0 ? void 0 : Number(args.limit);
|
|
40412
|
+
if (limit !== void 0 && !(Number.isInteger(limit) && limit > 0)) fail5("`--limit` is a whole number above zero");
|
|
40413
|
+
try {
|
|
40414
|
+
const response = await apiPost("/api/experiments/history", {
|
|
40415
|
+
...args.landing ? { landingSlug: String(args.landing) } : {},
|
|
40416
|
+
...limit === void 0 ? {} : { limit }
|
|
40417
|
+
});
|
|
40418
|
+
writeJsonEnvelope({
|
|
40419
|
+
ok: true,
|
|
40420
|
+
data: response.data,
|
|
40421
|
+
hints: buildHistoryHints(response.data.tests, args.landing === void 0 ? void 0 : String(args.landing))
|
|
40422
|
+
});
|
|
40423
|
+
} catch (error) {
|
|
40424
|
+
reportApiError(error);
|
|
40425
|
+
}
|
|
40426
|
+
}
|
|
40427
|
+
});
|
|
39411
40428
|
registerSchema({
|
|
39412
40429
|
command: "experiment.finish",
|
|
39413
40430
|
description: "End a test now and send all its traffic to the version you name",
|
|
@@ -39417,9 +40434,34 @@ registerSchema({
|
|
|
39417
40434
|
type: "string",
|
|
39418
40435
|
description: "Which version stays live: `original` or `variant`. Required \u2014 the verdict is a recommendation and this is the decision. A test that has not concluded can be ended too; it is recorded as inconclusive, never as a finding. Going against a verdict is allowed and is recorded as such.",
|
|
39419
40436
|
required: true
|
|
39420
|
-
}
|
|
40437
|
+
},
|
|
40438
|
+
reason: REASON_ARG,
|
|
40439
|
+
learning: LEARNING_ARG
|
|
39421
40440
|
}
|
|
39422
40441
|
});
|
|
40442
|
+
var END_REASONS = ["numbers_clear", "not_settling", "page_changing", "test_flawed"];
|
|
40443
|
+
function finishHints(data, given) {
|
|
40444
|
+
const { reason } = given;
|
|
40445
|
+
const args = { learning: given.learning };
|
|
40446
|
+
return [
|
|
40447
|
+
`Done. Everyone now sees the ${data.survivor === "variant" ? "new variant" : "original"}.`,
|
|
40448
|
+
...data.againstVerdict ? [
|
|
40449
|
+
`You kept the ${data.survivor === "variant" ? "new variant" : "original"} although the numbers favoured the other version (${data.verdict}). That is recorded on the test \u2014 say why in the write-up.`
|
|
40450
|
+
] : [],
|
|
40451
|
+
...data.verdict === "keep_running" ? [
|
|
40452
|
+
"This test had not concluded, so it is recorded as inconclusive \u2014 nobody found out. Never report it as no difference."
|
|
40453
|
+
] : [],
|
|
40454
|
+
...data.survivor === "variant" ? [
|
|
40455
|
+
"Now run `baker experiment fold` \u2014 it moves the winning version into the page itself and archives the other one. Until that lands the page is served by a rewrite, and it cannot be tested again."
|
|
40456
|
+
] : [],
|
|
40457
|
+
...args.learning || data.verdict === "keep_running" ? [] : [
|
|
40458
|
+
'Write down what this taught while it is fresh: `baker experiment update --id <id> --learning "\u2026"` \u2014 one or two sentences the client should believe about this page now. It is what the next test on this page starts from.'
|
|
40459
|
+
],
|
|
40460
|
+
...reason ? [] : [
|
|
40461
|
+
"No `--reason` was given, so the record says what the numbers had reached but not why anyone stopped looking. Next time pass one: numbers_clear, not_settling, page_changing or test_flawed."
|
|
40462
|
+
]
|
|
40463
|
+
];
|
|
40464
|
+
}
|
|
39423
40465
|
var finishCommand = defineCommand119({
|
|
39424
40466
|
meta: {
|
|
39425
40467
|
name: "finish",
|
|
@@ -39431,17 +40473,60 @@ var finishCommand = defineCommand119({
|
|
|
39431
40473
|
type: "string",
|
|
39432
40474
|
description: "Which version stays live: `original` or `variant`.",
|
|
39433
40475
|
required: true
|
|
39434
|
-
}
|
|
40476
|
+
},
|
|
40477
|
+
reason: REASON_ARG,
|
|
40478
|
+
learning: LEARNING_ARG
|
|
39435
40479
|
},
|
|
39436
40480
|
run: async ({ args }) => {
|
|
39437
40481
|
const keep = String(args.keep);
|
|
39438
40482
|
if (keep !== "original" && keep !== "variant") {
|
|
39439
40483
|
fail5("`--keep` is either `original` or `variant` \u2014 which version should everyone see from now on?");
|
|
39440
40484
|
}
|
|
40485
|
+
const reason = args.reason === void 0 ? void 0 : String(args.reason);
|
|
40486
|
+
if (reason !== void 0 && !END_REASONS.some((known) => known === reason)) {
|
|
40487
|
+
fail5(`\`--reason\` is one of ${END_REASONS.join(", ")} \u2014 why is this test being ended?`);
|
|
40488
|
+
}
|
|
39441
40489
|
try {
|
|
39442
40490
|
const response = await apiPost(
|
|
39443
40491
|
"/api/experiments/finish",
|
|
39444
|
-
{
|
|
40492
|
+
{
|
|
40493
|
+
experimentId: String(args.id),
|
|
40494
|
+
keep,
|
|
40495
|
+
...reason ? { reason } : {},
|
|
40496
|
+
...args.learning ? { learning: String(args.learning) } : {}
|
|
40497
|
+
}
|
|
40498
|
+
);
|
|
40499
|
+
if ("unstaged" in response.data) {
|
|
40500
|
+
writeJsonEnvelope({
|
|
40501
|
+
ok: true,
|
|
40502
|
+
data: response.data,
|
|
40503
|
+
hints: ["That test had not been published yet, so it was taken back off this session. Nothing was split."]
|
|
40504
|
+
});
|
|
40505
|
+
return;
|
|
40506
|
+
}
|
|
40507
|
+
const data = response.data;
|
|
40508
|
+
writeJsonEnvelope({ ok: true, data, hints: finishHints(data, { reason, learning: args.learning }) });
|
|
40509
|
+
} catch (error) {
|
|
40510
|
+
reportApiError(error);
|
|
40511
|
+
}
|
|
40512
|
+
}
|
|
40513
|
+
});
|
|
40514
|
+
registerSchema({
|
|
40515
|
+
command: "experiment.cancel",
|
|
40516
|
+
description: "Call a test off \u2014 take a staged one back off the session, or end a live one keeping the original",
|
|
40517
|
+
args: { id: ID_ARG }
|
|
40518
|
+
});
|
|
40519
|
+
var cancelCommand = defineCommand119({
|
|
40520
|
+
meta: {
|
|
40521
|
+
name: "cancel",
|
|
40522
|
+
description: "Call a test off. A test staged in this session and not yet published is simply taken back off it \u2014 nothing was split. A test that is live is ended now with everyone sent back to the original: what the numbers had said is recorded as it stood (usually `inconclusive`), and the variant page stays in the workspace, unserved. `finish --keep` is the same act with the choice of version; this is the shortcut for \u201Cstop, keep the page as it is\u201D."
|
|
40523
|
+
},
|
|
40524
|
+
args: { id: ID_ARG },
|
|
40525
|
+
run: async ({ args }) => {
|
|
40526
|
+
try {
|
|
40527
|
+
const response = await apiPost(
|
|
40528
|
+
"/api/experiments/finish",
|
|
40529
|
+
{ experimentId: String(args.id), keep: "original" }
|
|
39445
40530
|
);
|
|
39446
40531
|
if ("unstaged" in response.data) {
|
|
39447
40532
|
writeJsonEnvelope({
|
|
@@ -39456,16 +40541,68 @@ var finishCommand = defineCommand119({
|
|
|
39456
40541
|
ok: true,
|
|
39457
40542
|
data,
|
|
39458
40543
|
hints: [
|
|
39459
|
-
|
|
39460
|
-
...data.againstVerdict ? [
|
|
39461
|
-
`You kept the ${data.survivor === "variant" ? "new variant" : "original"} although the numbers favoured the other version (${data.verdict}). That is recorded on the test \u2014 say why in the write-up.`
|
|
39462
|
-
] : [],
|
|
40544
|
+
"Ended. Everyone sees the original page now, and the split stopped within seconds.",
|
|
39463
40545
|
...data.verdict === "keep_running" ? [
|
|
39464
|
-
"
|
|
40546
|
+
"It had not concluded, so it is recorded as inconclusive \u2014 nobody found out. Never report it as no difference."
|
|
40547
|
+
] : data.againstVerdict ? [
|
|
40548
|
+
`The numbers favoured the new variant (${data.verdict}), so this is recorded as a decision against them. Say why in the write-up.`
|
|
39465
40549
|
] : [],
|
|
39466
|
-
|
|
39467
|
-
|
|
39468
|
-
|
|
40550
|
+
"The variant page is still in the workspace and served to nobody. Run `baker experiment fold` to archive it, or leave it for a later test."
|
|
40551
|
+
]
|
|
40552
|
+
});
|
|
40553
|
+
} catch (error) {
|
|
40554
|
+
reportApiError(error);
|
|
40555
|
+
}
|
|
40556
|
+
}
|
|
40557
|
+
});
|
|
40558
|
+
registerSchema({
|
|
40559
|
+
command: "experiment.update",
|
|
40560
|
+
description: "Change what a test says about itself \u2014 its wording always, its measurement only while it is still staged",
|
|
40561
|
+
args: {
|
|
40562
|
+
id: ID_ARG,
|
|
40563
|
+
because: { ...BECAUSE_ARG, required: false },
|
|
40564
|
+
change: { ...CHANGE_ARG, required: false },
|
|
40565
|
+
evidence: EVIDENCE_ARG,
|
|
40566
|
+
expect: { ...EXPECT_ARG, description: "Staged tests only. `increase` or `decrease`." },
|
|
40567
|
+
goal: { ...GOAL_ARG, description: "Staged tests only. The event the test is read on, by key." },
|
|
40568
|
+
variantGoal: {
|
|
40569
|
+
...VARIANT_GOAL_ARG,
|
|
40570
|
+
description: "Staged tests only. The new version's own event, when it differs."
|
|
40571
|
+
},
|
|
40572
|
+
lift: { ...LIFT_ARG, description: "Staged tests only. The smallest lift worth detecting." },
|
|
40573
|
+
learning: LEARNING_ARG
|
|
40574
|
+
}
|
|
40575
|
+
});
|
|
40576
|
+
var updateCommand3 = defineCommand119({
|
|
40577
|
+
meta: {
|
|
40578
|
+
name: "update",
|
|
40579
|
+
description: "Change what a test says about itself. The observation (`--because`), the change in the client's words (`--change`) and the evidence cited (`--evidence`) can change at any time \u2014 they describe the test, and a write-up corrected after the fact is still the write-up of the same test. On a FINISHED test, `--learning` writes down what it taught. What the test MEASURES \u2014 `--goal`, `--lift`, `--expect` \u2014 can only change while it is still staged in this session: once visitors have been split, moving the goal or the finish line is how a result gets argued into existence. To measure something else on a live test, `finish` it and start a new one."
|
|
40580
|
+
},
|
|
40581
|
+
args: {
|
|
40582
|
+
id: ID_ARG,
|
|
40583
|
+
because: { ...BECAUSE_ARG, required: false },
|
|
40584
|
+
change: { ...CHANGE_ARG, required: false },
|
|
40585
|
+
evidence: EVIDENCE_ARG,
|
|
40586
|
+
expect: { ...EXPECT_ARG, description: "Staged tests only. `increase` or `decrease`." },
|
|
40587
|
+
goal: { ...GOAL_ARG, description: "Staged tests only. The event the test is read on, by key." },
|
|
40588
|
+
variantGoal: {
|
|
40589
|
+
...VARIANT_GOAL_ARG,
|
|
40590
|
+
description: "Staged tests only. The new version's own event, when it differs."
|
|
40591
|
+
},
|
|
40592
|
+
lift: { ...LIFT_ARG, description: "Staged tests only. The smallest lift worth detecting." },
|
|
40593
|
+
learning: LEARNING_ARG
|
|
40594
|
+
},
|
|
40595
|
+
run: async ({ args, rawArgs }) => {
|
|
40596
|
+
const request = buildUpdateRequest(args, rawArgs);
|
|
40597
|
+
if ("error" in request) fail5(request.error);
|
|
40598
|
+
try {
|
|
40599
|
+
const response = await apiPost("/api/experiments/update", request);
|
|
40600
|
+
writeJsonEnvelope({
|
|
40601
|
+
ok: true,
|
|
40602
|
+
data: response.data,
|
|
40603
|
+
hints: [
|
|
40604
|
+
response.data.staged ? "Updated and re-sized. It still starts when this session is published." : request.learning !== void 0 ? "Updated. What this test taught is on its record now, and `plan` hands it to the next test on this page." : "Updated. The test keeps running exactly as it was \u2014 only its wording changed.",
|
|
40605
|
+
`It now reads: ${response.data.hypothesisStatement}`
|
|
39469
40606
|
]
|
|
39470
40607
|
});
|
|
39471
40608
|
} catch (error) {
|
|
@@ -39516,7 +40653,7 @@ var resumeCommand = defineCommand119({
|
|
|
39516
40653
|
writeJsonEnvelope({
|
|
39517
40654
|
ok: true,
|
|
39518
40655
|
data: { resumed: true },
|
|
39519
|
-
hints: ["Resumed.
|
|
40656
|
+
hints: ["Resumed. The split is back on, with the same sides."]
|
|
39520
40657
|
});
|
|
39521
40658
|
} catch (error) {
|
|
39522
40659
|
reportApiError(error);
|
|
@@ -39562,6 +40699,7 @@ var foldCommand = defineCommand119({
|
|
|
39562
40699
|
if (folded.length > 0) {
|
|
39563
40700
|
await apiPost("/api/experiments/fold", {
|
|
39564
40701
|
applied: true,
|
|
40702
|
+
experimentIds: folded.map((result) => result.experimentId),
|
|
39565
40703
|
...args.id ? { experimentId: String(args.id) } : {}
|
|
39566
40704
|
});
|
|
39567
40705
|
}
|
|
@@ -39589,10 +40727,13 @@ var experimentCommand = defineCommand119({
|
|
|
39589
40727
|
subCommands: {
|
|
39590
40728
|
plan: planCommand,
|
|
39591
40729
|
start: startCommand,
|
|
40730
|
+
update: updateCommand3,
|
|
39592
40731
|
status: statusCommand6,
|
|
40732
|
+
history: historyCommand,
|
|
39593
40733
|
pause: pauseCommand,
|
|
39594
40734
|
resume: resumeCommand,
|
|
39595
40735
|
finish: finishCommand,
|
|
40736
|
+
cancel: cancelCommand,
|
|
39596
40737
|
fold: foldCommand
|
|
39597
40738
|
}
|
|
39598
40739
|
});
|
|
@@ -39603,176 +40744,6 @@ import { defineCommand as defineCommand124 } from "citty";
|
|
|
39603
40744
|
// src/commands/flows/add.ts
|
|
39604
40745
|
import { randomUUID } from "crypto";
|
|
39605
40746
|
import { defineCommand as defineCommand120 } from "citty";
|
|
39606
|
-
|
|
39607
|
-
// src/commands/flows/shared.ts
|
|
39608
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "fs";
|
|
39609
|
-
import { join as join3 } from "path";
|
|
39610
|
-
var FLOWS_DIR = "src/lib/flow-engine/flows";
|
|
39611
|
-
function flowsDir() {
|
|
39612
|
-
let dir = process.cwd();
|
|
39613
|
-
for (let i = 0; i < 6; i++) {
|
|
39614
|
-
const candidate = join3(dir, FLOWS_DIR);
|
|
39615
|
-
if (existsSync5(candidate)) {
|
|
39616
|
-
return candidate;
|
|
39617
|
-
}
|
|
39618
|
-
const parent = join3(dir, "..");
|
|
39619
|
-
if (parent === dir) {
|
|
39620
|
-
break;
|
|
39621
|
-
}
|
|
39622
|
-
dir = parent;
|
|
39623
|
-
}
|
|
39624
|
-
return join3(process.cwd(), FLOWS_DIR);
|
|
39625
|
-
}
|
|
39626
|
-
function failLocal(message) {
|
|
39627
|
-
writeJson({ ok: false, error: { code: "NOT_FOUND", message } });
|
|
39628
|
-
process.exit(1);
|
|
39629
|
-
}
|
|
39630
|
-
function listFlowSlugs() {
|
|
39631
|
-
const dir = flowsDir();
|
|
39632
|
-
if (!existsSync5(dir)) {
|
|
39633
|
-
return [];
|
|
39634
|
-
}
|
|
39635
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
|
|
39636
|
-
}
|
|
39637
|
-
function flowDataPath(slug) {
|
|
39638
|
-
return join3(flowsDir(), slug, "_data.json");
|
|
39639
|
-
}
|
|
39640
|
-
function writeFlowTree(slug, tree) {
|
|
39641
|
-
writeFileSync3(flowDataPath(slug), `${JSON.stringify(tree, null, 2)}
|
|
39642
|
-
`, "utf-8");
|
|
39643
|
-
}
|
|
39644
|
-
function walkNodes(node, acc = []) {
|
|
39645
|
-
if (!node || typeof node !== "object") return acc;
|
|
39646
|
-
acc.push(node);
|
|
39647
|
-
if (Array.isArray(node.children)) {
|
|
39648
|
-
for (const child of node.children) walkNodes(child, acc);
|
|
39649
|
-
}
|
|
39650
|
-
return acc;
|
|
39651
|
-
}
|
|
39652
|
-
function collectSideEffects(tree) {
|
|
39653
|
-
return walkNodes(tree).flatMap(
|
|
39654
|
-
(node) => Array.isArray(node.sideEffects) ? node.sideEffects.filter((sideEffect) => Boolean(sideEffect) && typeof sideEffect === "object").map((sideEffect) => ({ node, sideEffect })) : []
|
|
39655
|
-
);
|
|
39656
|
-
}
|
|
39657
|
-
function readFlowTree(slug) {
|
|
39658
|
-
const path42 = join3(flowsDir(), slug, "_data.json");
|
|
39659
|
-
if (!existsSync5(path42)) {
|
|
39660
|
-
failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
|
|
39661
|
-
}
|
|
39662
|
-
try {
|
|
39663
|
-
return JSON.parse(readFileSync11(path42, "utf-8"));
|
|
39664
|
-
} catch (error) {
|
|
39665
|
-
failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
39666
|
-
}
|
|
39667
|
-
}
|
|
39668
|
-
function isConfigured(value) {
|
|
39669
|
-
return typeof value === "string" && value.length > 0;
|
|
39670
|
-
}
|
|
39671
|
-
function nodeMeta(record) {
|
|
39672
|
-
const nodeId = typeof record.id === "string" ? record.id : "";
|
|
39673
|
-
const nodeName = typeof record.name === "string" ? record.name : void 0;
|
|
39674
|
-
return { nodeId, ...nodeName ? { nodeName } : {} };
|
|
39675
|
-
}
|
|
39676
|
-
function widgetNodeStatus(record, meta) {
|
|
39677
|
-
const nodeData = record.nodeData;
|
|
39678
|
-
const nodeType = nodeData?.type;
|
|
39679
|
-
if (typeof nodeType !== "string" || !FLOW_RESOURCE_NODE_TYPES.includes(nodeType)) {
|
|
39680
|
-
return null;
|
|
39681
|
-
}
|
|
39682
|
-
const external = nodeData?.form?.external;
|
|
39683
|
-
const selected = external != null && typeof external === "object";
|
|
39684
|
-
const name = selected ? external.name : void 0;
|
|
39685
|
-
return {
|
|
39686
|
-
...meta,
|
|
39687
|
-
target: "node",
|
|
39688
|
-
kind: nodeType,
|
|
39689
|
-
status: selected ? `${nodeType} \u2014 selected${typeof name === "string" ? ` "${name}"` : ""}` : `${nodeType} \u2014 not selected`,
|
|
39690
|
-
needsInput: !selected
|
|
39691
|
-
};
|
|
39692
|
-
}
|
|
39693
|
-
function sideEffectNeedsInput(raw) {
|
|
39694
|
-
const effect = raw;
|
|
39695
|
-
const type = effect?.type;
|
|
39696
|
-
if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
|
|
39697
|
-
return false;
|
|
39698
|
-
}
|
|
39699
|
-
const configured = isConfigured(effect?.encryptedConfig);
|
|
39700
|
-
if (!isFlowOauthSideEffect(type)) return !configured;
|
|
39701
|
-
return !isConfigured(effect?.oauthProviderId) || !configured;
|
|
39702
|
-
}
|
|
39703
|
-
function sideEffectStatus(raw, meta) {
|
|
39704
|
-
const effect = raw;
|
|
39705
|
-
const type = effect?.type;
|
|
39706
|
-
if (typeof type !== "string" || !FLOW_SECRET_SIDE_EFFECT_TYPES.includes(type)) {
|
|
39707
|
-
return null;
|
|
39708
|
-
}
|
|
39709
|
-
const configured = isConfigured(effect?.encryptedConfig);
|
|
39710
|
-
const oauth = isFlowOauthSideEffect(type);
|
|
39711
|
-
const connected = isConfigured(effect?.oauthProviderId);
|
|
39712
|
-
const needs = FLOW_SECRET_FIELDS[type];
|
|
39713
|
-
const status = oauth ? `${type} \u2014 connection ${connected ? "[connected]" : "[needs connection]"}, config ${configured ? "[set]" : "[missing]"}` : configured ? `${type} \u2014 [configured]` : `${type} \u2014 [missing]${needs.length > 0 ? ` (needs: ${needs.join(", ")})` : ""}`;
|
|
39714
|
-
return {
|
|
39715
|
-
...meta,
|
|
39716
|
-
target: "sideEffect",
|
|
39717
|
-
...typeof effect?.id === "string" ? { sideEffectId: effect.id } : {},
|
|
39718
|
-
kind: type,
|
|
39719
|
-
status,
|
|
39720
|
-
needsInput: sideEffectNeedsInput(effect)
|
|
39721
|
-
};
|
|
39722
|
-
}
|
|
39723
|
-
function requestFlowInputCall(slug, status) {
|
|
39724
|
-
const args = status.target === "node" ? `{ target: "node", flowSlug: "${slug}", nodeId: "${status.nodeId}", nodeType: "${status.kind}" }` : `{ target: "sideEffect", flowSlug: "${slug}", nodeId: "${status.nodeId}", sideEffectId: "${status.sideEffectId}", sideEffectType: "${status.kind}" }`;
|
|
39725
|
-
return `request_flow_input with ${args}`;
|
|
39726
|
-
}
|
|
39727
|
-
function collectConfigStatus(node, acc) {
|
|
39728
|
-
if (!node || typeof node !== "object") {
|
|
39729
|
-
return;
|
|
39730
|
-
}
|
|
39731
|
-
const record = node;
|
|
39732
|
-
const meta = nodeMeta(record);
|
|
39733
|
-
const widget = widgetNodeStatus(record, meta);
|
|
39734
|
-
if (widget) {
|
|
39735
|
-
acc.push(widget);
|
|
39736
|
-
}
|
|
39737
|
-
if (Array.isArray(record.sideEffects)) {
|
|
39738
|
-
for (const raw of record.sideEffects) {
|
|
39739
|
-
const status = sideEffectStatus(raw, meta);
|
|
39740
|
-
if (status) {
|
|
39741
|
-
acc.push(status);
|
|
39742
|
-
}
|
|
39743
|
-
}
|
|
39744
|
-
}
|
|
39745
|
-
if (Array.isArray(record.children)) {
|
|
39746
|
-
for (const child of record.children) {
|
|
39747
|
-
collectConfigStatus(child, acc);
|
|
39748
|
-
}
|
|
39749
|
-
}
|
|
39750
|
-
}
|
|
39751
|
-
function redactTree(node) {
|
|
39752
|
-
if (Array.isArray(node)) {
|
|
39753
|
-
return node.map(redactTree);
|
|
39754
|
-
}
|
|
39755
|
-
if (!node || typeof node !== "object") {
|
|
39756
|
-
return node;
|
|
39757
|
-
}
|
|
39758
|
-
const out = {};
|
|
39759
|
-
for (const [key, value] of Object.entries(node)) {
|
|
39760
|
-
if (key === "encryptedConfig") {
|
|
39761
|
-
out[key] = isConfigured(value) ? "[configured]" : value;
|
|
39762
|
-
} else {
|
|
39763
|
-
out[key] = redactTree(value);
|
|
39764
|
-
}
|
|
39765
|
-
}
|
|
39766
|
-
return out;
|
|
39767
|
-
}
|
|
39768
|
-
function stepNameKey(name) {
|
|
39769
|
-
return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
|
|
39770
|
-
}
|
|
39771
|
-
function isUsableStepNameKey(key) {
|
|
39772
|
-
return /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(key);
|
|
39773
|
-
}
|
|
39774
|
-
|
|
39775
|
-
// src/commands/flows/add.ts
|
|
39776
40747
|
function unknownType(kind, type) {
|
|
39777
40748
|
writeJson({
|
|
39778
40749
|
ok: false,
|
|
@@ -39863,6 +40834,10 @@ var addNodeCommand = defineCommand120({
|
|
|
39863
40834
|
data: { slug, nodeId: node.id, type, parentId: parent.id ?? null, path: flowDataPath(slug) },
|
|
39864
40835
|
hints: [
|
|
39865
40836
|
`Now fill in this step's \`ai\` field and its copy by editing ${flowDataPath(slug)}.`,
|
|
40837
|
+
// Said on every step added, because the moment a Form's shape changes
|
|
40838
|
+
// is the moment its outcome can move — and a Form nobody named an
|
|
40839
|
+
// outcome for reports zero conversions rather than an error.
|
|
40840
|
+
`Say what this Form counts before you finish: \`baker analytics conversions --flow ${slug}\` lists every ending already spelled as an event key, including this step's. Nothing in _data.json decides it.`,
|
|
39866
40841
|
...entry.owners?.["nodeData.form.external"] === "request_flow_input" ? [
|
|
39867
40842
|
`A ${type} node's resource is picked by the user: call request_flow_input with { target: "node", flowSlug: "${slug}", nodeId: "${node.id}", nodeType: "${type}" }.`
|
|
39868
40843
|
] : [],
|
|
@@ -39976,6 +40951,13 @@ var addSideEffectCommand = defineCommand120({
|
|
|
39976
40951
|
data: { slug, nodeId: node.id, sideEffectId: id, type, triggerId, path: flowDataPath(slug) },
|
|
39977
40952
|
hints: [
|
|
39978
40953
|
`Fires on \`${triggerId}\` \u2014 the moment "${String(node.name ?? node.id)}" raises. A side effect only runs when its triggerId is the one its own step raises, so this is set from the step, never from the side-effect type.`,
|
|
40954
|
+
// A pixel side effect is the thing most often mistaken for "setting the
|
|
40955
|
+
// conversion". It tells the ad platform; it tells Baker nothing, and a
|
|
40956
|
+
// Form can fire every pixel it has while every Baker report says it
|
|
40957
|
+
// converted nobody.
|
|
40958
|
+
...needsUser.some(([, owner]) => owner === "tags") ? [
|
|
40959
|
+
`This reports the conversion to ${type}. It does NOT make Baker count anything: what Baker counts is a named event, set with \`baker analytics conversions --flow ${slug}\`. Do both \u2014 they are two different systems asked the same question.`
|
|
40960
|
+
] : [],
|
|
39979
40961
|
...needsUser.some(([, owner]) => owner === "request_flow_input") ? [
|
|
39980
40962
|
`Configure it: request_flow_input with { target: "sideEffect", flowSlug: "${slug}", nodeId: "${node.id}", sideEffectId: "${id}", sideEffectType: "${type}" }. Read the destination straight off that call's result.`
|
|
39981
40963
|
] : [],
|
|
@@ -39989,11 +40971,11 @@ var addSideEffectCommand = defineCommand120({
|
|
|
39989
40971
|
});
|
|
39990
40972
|
|
|
39991
40973
|
// src/commands/flows/map.ts
|
|
39992
|
-
import { readFileSync as
|
|
40974
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
39993
40975
|
import { defineCommand as defineCommand121 } from "citty";
|
|
39994
40976
|
|
|
39995
40977
|
// src/commands/flows/value-expression.ts
|
|
39996
|
-
import { existsSync as existsSync6, readFileSync as
|
|
40978
|
+
import { existsSync as existsSync6, readFileSync as readFileSync13 } from "fs";
|
|
39997
40979
|
import { join as join4 } from "path";
|
|
39998
40980
|
var PREFIXES = ["field", "tracking", "global", "code"];
|
|
39999
40981
|
function splitParts(raw) {
|
|
@@ -40052,10 +41034,10 @@ function parseValueExpression(raw) {
|
|
|
40052
41034
|
return parts.map(parsePart);
|
|
40053
41035
|
}
|
|
40054
41036
|
function trackingFieldIds() {
|
|
40055
|
-
const
|
|
40056
|
-
if (!existsSync6(
|
|
41037
|
+
const path44 = join4(flowsDir(), "..", "tracking.ts");
|
|
41038
|
+
if (!existsSync6(path44)) return null;
|
|
40057
41039
|
try {
|
|
40058
|
-
const source =
|
|
41040
|
+
const source = readFileSync13(path44, "utf-8");
|
|
40059
41041
|
const block2 = source.match(/TRACKING_FIELD_IDS\s*=\s*\[([\s\S]*?)\]\s*as const/)?.[1];
|
|
40060
41042
|
if (!block2) return null;
|
|
40061
41043
|
const ids = [...block2.matchAll(/"(tracking\.[a-z0-9_]+)"/g)].map((match) => match[1]);
|
|
@@ -40596,13 +41578,13 @@ function specsFromFile(parsed) {
|
|
|
40596
41578
|
return `${destField}${type}=${entry?.value ?? ""}`;
|
|
40597
41579
|
});
|
|
40598
41580
|
}
|
|
40599
|
-
function readSpecFile(
|
|
41581
|
+
function readSpecFile(path44) {
|
|
40600
41582
|
let raw;
|
|
40601
41583
|
try {
|
|
40602
|
-
raw =
|
|
41584
|
+
raw = path44 === "-" ? readFileSync14(0, "utf-8") : readFileSync14(path44, "utf-8");
|
|
40603
41585
|
} catch (error) {
|
|
40604
41586
|
refuse(
|
|
40605
|
-
`Could not read ${
|
|
41587
|
+
`Could not read ${path44 === "-" ? "the mapping from stdin" : `"${path44}"`}: ${error instanceof Error ? error.message : String(error)}`
|
|
40606
41588
|
);
|
|
40607
41589
|
}
|
|
40608
41590
|
let parsed;
|
|
@@ -40610,7 +41592,7 @@ function readSpecFile(path42) {
|
|
|
40610
41592
|
parsed = JSON.parse(raw);
|
|
40611
41593
|
} catch (error) {
|
|
40612
41594
|
refuse(
|
|
40613
|
-
`${
|
|
41595
|
+
`${path44 === "-" ? "stdin" : `"${path44}"`} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
40614
41596
|
'Expected { "map": { "<destField>": "<value>", \u2026 } }'
|
|
40615
41597
|
);
|
|
40616
41598
|
}
|
|
@@ -40880,18 +41862,18 @@ var ARRAY_FIELDS = [
|
|
|
40880
41862
|
"tagIds"
|
|
40881
41863
|
];
|
|
40882
41864
|
var ARRAY_OWNERS = ["", "body"];
|
|
40883
|
-
function dropUnsetOptionals(sideEffect,
|
|
41865
|
+
function dropUnsetOptionals(sideEffect, path44) {
|
|
40884
41866
|
return OPTIONAL_STRINGS.flatMap((key) => {
|
|
40885
41867
|
if (!(key in sideEffect) || sideEffect[key] !== null && sideEffect[key] !== "") return [];
|
|
40886
41868
|
delete sideEffect[key];
|
|
40887
|
-
return [{ path:
|
|
41869
|
+
return [{ path: path44, change: `dropped \`${key}\` (an optional string is absent, never null)` }];
|
|
40888
41870
|
});
|
|
40889
41871
|
}
|
|
40890
|
-
function fillNulledArrays(target, prefix,
|
|
41872
|
+
function fillNulledArrays(target, prefix, path44) {
|
|
40891
41873
|
return ARRAY_FIELDS.flatMap((key) => {
|
|
40892
41874
|
if (!(key in target) || target[key] !== null) return [];
|
|
40893
41875
|
target[key] = [];
|
|
40894
|
-
return [{ path:
|
|
41876
|
+
return [{ path: path44, change: `\`${prefix}${key}: null\` \u2192 \`[]\`` }];
|
|
40895
41877
|
});
|
|
40896
41878
|
}
|
|
40897
41879
|
function sideEffectsOf(node) {
|
|
@@ -40901,13 +41883,13 @@ function sideEffectsOf(node) {
|
|
|
40901
41883
|
);
|
|
40902
41884
|
}
|
|
40903
41885
|
function normalizeSideEffect(sideEffect, where) {
|
|
40904
|
-
const
|
|
41886
|
+
const path44 = `${where} \u2192 ${String(sideEffect.id ?? "side effect")}`;
|
|
40905
41887
|
const arrays = ARRAY_OWNERS.flatMap((owner) => {
|
|
40906
41888
|
const target = owner ? sideEffect[owner] : sideEffect;
|
|
40907
41889
|
if (!target || typeof target !== "object") return [];
|
|
40908
|
-
return fillNulledArrays(target, owner ? `${owner}.` : "",
|
|
41890
|
+
return fillNulledArrays(target, owner ? `${owner}.` : "", path44);
|
|
40909
41891
|
});
|
|
40910
|
-
return [...dropUnsetOptionals(sideEffect,
|
|
41892
|
+
return [...dropUnsetOptionals(sideEffect, path44), ...arrays];
|
|
40911
41893
|
}
|
|
40912
41894
|
function normalizeFlowTree(tree) {
|
|
40913
41895
|
const changes = [];
|
|
@@ -41140,9 +42122,15 @@ var listCommand13 = defineCommand124({
|
|
|
41140
42122
|
writeJson({
|
|
41141
42123
|
ok: true,
|
|
41142
42124
|
data: { flows },
|
|
41143
|
-
hints:
|
|
41144
|
-
|
|
41145
|
-
|
|
42125
|
+
hints: [
|
|
42126
|
+
...pending.length > 0 ? [
|
|
42127
|
+
`Still waiting on the user: ${pending.map((flow) => `${flow.slug} (${flow.needsConfig})`).join(", ")}. \`baker flows show <slug>\` returns the request_flow_input call for each.`
|
|
42128
|
+
] : [],
|
|
42129
|
+
// "Set the conversions on the Forms" lands here first, and nothing in
|
|
42130
|
+
// this family can do it — the conversion side effects it does offer
|
|
42131
|
+
// fire a pixel and are not what any Baker report counts.
|
|
42132
|
+
"Setting what a Form COUNTS is not in this family: a conversion is a named event held in Baker. `baker analytics conversions` lists what this company counts; `--flow <slug>` spells one Form's endings as keys you can count, traffic or no traffic."
|
|
42133
|
+
]
|
|
41146
42134
|
});
|
|
41147
42135
|
}
|
|
41148
42136
|
});
|
|
@@ -41170,7 +42158,15 @@ var showCommand4 = defineCommand124({
|
|
|
41170
42158
|
data: response,
|
|
41171
42159
|
// Inert side effects first: an unconfigured destination is work still to
|
|
41172
42160
|
// do, but a side effect wired to the wrong moment reads as done.
|
|
41173
|
-
hints: [
|
|
42161
|
+
hints: [
|
|
42162
|
+
...inertSideEffectHints(tree),
|
|
42163
|
+
...pendingInputHints(slug, config),
|
|
42164
|
+
// The question this command is asked — "is this Form wired up?" — has
|
|
42165
|
+
// an answer that is not in this file at all. What a Form counts is a
|
|
42166
|
+
// named event in Baker, and a Form can be perfectly configured, fire
|
|
42167
|
+
// every pixel it has, and still report converting nobody.
|
|
42168
|
+
`What this Form counts as a conversion is NOT in _data.json and not in any side effect here: it is a named event held in Baker. Read it, and this Form's own endings spelled as event keys, with \`baker analytics conversions --flow ${slug}\`.`
|
|
42169
|
+
]
|
|
41174
42170
|
});
|
|
41175
42171
|
}
|
|
41176
42172
|
});
|
|
@@ -41335,7 +42331,7 @@ Examples:
|
|
|
41335
42331
|
import { defineCommand as defineCommand126 } from "citty";
|
|
41336
42332
|
|
|
41337
42333
|
// src/commands/ga4/shared.ts
|
|
41338
|
-
import { readFileSync as
|
|
42334
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
41339
42335
|
function failValidation3(message) {
|
|
41340
42336
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
41341
42337
|
process.exit(1);
|
|
@@ -41351,7 +42347,7 @@ function requireTarget5(args, entity) {
|
|
|
41351
42347
|
function readJsonSource(args, flag) {
|
|
41352
42348
|
const inline = args[flag];
|
|
41353
42349
|
const file = args.file;
|
|
41354
|
-
const raw = typeof file === "string" && file.length > 0 ?
|
|
42350
|
+
const raw = typeof file === "string" && file.length > 0 ? readFileSync15(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
|
|
41355
42351
|
if (raw === void 0) {
|
|
41356
42352
|
failValidation3(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
|
|
41357
42353
|
}
|
|
@@ -41445,10 +42441,10 @@ async function stageOps(ops) {
|
|
|
41445
42441
|
handleError2(err);
|
|
41446
42442
|
}
|
|
41447
42443
|
}
|
|
41448
|
-
async function draftAction2(
|
|
42444
|
+
async function draftAction2(path44, body, chat) {
|
|
41449
42445
|
const chatId = resolveChatId(chat);
|
|
41450
42446
|
try {
|
|
41451
|
-
const data = await apiPost(
|
|
42447
|
+
const data = await apiPost(path44, { chatId, ...body });
|
|
41452
42448
|
writeJsonEnvelope({ ok: true, data });
|
|
41453
42449
|
return data;
|
|
41454
42450
|
} catch (err) {
|
|
@@ -41702,7 +42698,7 @@ Examples:
|
|
|
41702
42698
|
});
|
|
41703
42699
|
|
|
41704
42700
|
// src/commands/ga4/query.ts
|
|
41705
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as
|
|
42701
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync4 } from "fs";
|
|
41706
42702
|
import { resolve as resolve2 } from "path";
|
|
41707
42703
|
import { defineCommand as defineCommand129 } from "citty";
|
|
41708
42704
|
|
|
@@ -41793,7 +42789,7 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
41793
42789
|
writeFileSync4(filePath, content, "utf-8");
|
|
41794
42790
|
}
|
|
41795
42791
|
} else if (append && existsSync7(filePath)) {
|
|
41796
|
-
const existing = JSON.parse(
|
|
42792
|
+
const existing = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
41797
42793
|
writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
41798
42794
|
} else {
|
|
41799
42795
|
writeFileSync4(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
@@ -42645,7 +43641,7 @@ Full guide: __tooling__/docs/tools/baker/ga4.md`
|
|
|
42645
43641
|
import { defineCommand as defineCommand135 } from "citty";
|
|
42646
43642
|
|
|
42647
43643
|
// src/commands/gsc/query.ts
|
|
42648
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as
|
|
43644
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "fs";
|
|
42649
43645
|
import { resolve as resolve3 } from "path";
|
|
42650
43646
|
import { defineCommand as defineCommand132 } from "citty";
|
|
42651
43647
|
|
|
@@ -42793,7 +43789,7 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
42793
43789
|
writeFileSync5(filePath, content, "utf-8");
|
|
42794
43790
|
}
|
|
42795
43791
|
} else if (append && existsSync8(filePath)) {
|
|
42796
|
-
const existing = JSON.parse(
|
|
43792
|
+
const existing = JSON.parse(readFileSync17(filePath, "utf-8"));
|
|
42797
43793
|
writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
42798
43794
|
} else {
|
|
42799
43795
|
writeFileSync5(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
@@ -43170,7 +44166,7 @@ var listCommand14 = defineCommand136({
|
|
|
43170
44166
|
}
|
|
43171
44167
|
}
|
|
43172
44168
|
});
|
|
43173
|
-
var
|
|
44169
|
+
var historyCommand2 = defineCommand136({
|
|
43174
44170
|
meta: {
|
|
43175
44171
|
name: "history",
|
|
43176
44172
|
description: `Unified account history (audit log): what changed, who did it, and when.
|
|
@@ -43862,7 +44858,7 @@ function cropSprite(input, region) {
|
|
|
43862
44858
|
|
|
43863
44859
|
// src/lib/image/io.ts
|
|
43864
44860
|
import { randomBytes } from "crypto";
|
|
43865
|
-
import { glob as fsGlob, readFile as
|
|
44861
|
+
import { glob as fsGlob, readFile as readFile24, rename, stat as stat6, writeFile as writeFile12 } from "fs/promises";
|
|
43866
44862
|
import { dirname as dirname2, extname as extname2, join as join5, resolve as resolve4 } from "path";
|
|
43867
44863
|
var REMOTE_RE = /^https?:\/\//i;
|
|
43868
44864
|
var GLOB_RE = /[*?[\]{}]/;
|
|
@@ -43895,11 +44891,11 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
43895
44891
|
const { buffer } = await fetchExternalBytes(pathOrUrl, { maxBytes: MAX_REMOTE_IMAGE_BYTES });
|
|
43896
44892
|
return buffer;
|
|
43897
44893
|
}
|
|
43898
|
-
return
|
|
44894
|
+
return readFile24(pathOrUrl);
|
|
43899
44895
|
}
|
|
43900
|
-
async function isDirectory(
|
|
44896
|
+
async function isDirectory(path44) {
|
|
43901
44897
|
try {
|
|
43902
|
-
const s = await stat6(
|
|
44898
|
+
const s = await stat6(path44);
|
|
43903
44899
|
return s.isDirectory();
|
|
43904
44900
|
} catch {
|
|
43905
44901
|
return false;
|
|
@@ -44218,13 +45214,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
|
|
|
44218
45214
|
}
|
|
44219
45215
|
function disambiguate(paths) {
|
|
44220
45216
|
const taken = /* @__PURE__ */ new Set();
|
|
44221
|
-
return paths.map((
|
|
44222
|
-
if (!taken.has(
|
|
44223
|
-
taken.add(
|
|
44224
|
-
return
|
|
45217
|
+
return paths.map((path44) => {
|
|
45218
|
+
if (!taken.has(path44)) {
|
|
45219
|
+
taken.add(path44);
|
|
45220
|
+
return path44;
|
|
44225
45221
|
}
|
|
44226
|
-
const ext = extname3(
|
|
44227
|
-
const stem =
|
|
45222
|
+
const ext = extname3(path44);
|
|
45223
|
+
const stem = path44.slice(0, path44.length - ext.length);
|
|
44228
45224
|
let n = 2;
|
|
44229
45225
|
while (taken.has(`${stem}-${n}${ext}`)) n += 1;
|
|
44230
45226
|
const unique = `${stem}-${n}${ext}`;
|
|
@@ -44350,10 +45346,10 @@ async function runDownloads(plan) {
|
|
|
44350
45346
|
const paths = disambiguate(fetched.map((item) => item.path));
|
|
44351
45347
|
const downloaded = [];
|
|
44352
45348
|
for (const [index, item] of fetched.entries()) {
|
|
44353
|
-
const
|
|
45349
|
+
const path44 = paths[index] ?? item.path;
|
|
44354
45350
|
try {
|
|
44355
|
-
await atomicWrite(
|
|
44356
|
-
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 });
|
|
44357
45353
|
} catch (err) {
|
|
44358
45354
|
failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
|
|
44359
45355
|
}
|
|
@@ -47317,8 +48313,8 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
47317
48313
|
import { defineCommand as defineCommand175 } from "citty";
|
|
47318
48314
|
|
|
47319
48315
|
// src/commands/landing/critique.ts
|
|
47320
|
-
import { readdir as
|
|
47321
|
-
import
|
|
48316
|
+
import { readdir as readdir12, readFile as readFile26, stat as stat8 } from "fs/promises";
|
|
48317
|
+
import path33 from "path";
|
|
47322
48318
|
import { defineCommand as defineCommand164 } from "citty";
|
|
47323
48319
|
|
|
47324
48320
|
// src/engine/landing/lib/constants.ts
|
|
@@ -48496,13 +49492,13 @@ function describeCounts(findings) {
|
|
|
48496
49492
|
|
|
48497
49493
|
// src/commands/landing/snapshot.ts
|
|
48498
49494
|
import { mkdir as mkdir9, rename as rename2, writeFile as writeFile13 } from "fs/promises";
|
|
48499
|
-
import
|
|
49495
|
+
import path31 from "path";
|
|
48500
49496
|
var CRITIC_VERSION = "3";
|
|
48501
49497
|
function critiqueCacheDir(projectRoot) {
|
|
48502
|
-
return
|
|
49498
|
+
return path31.join(projectRoot, ".cache", "landing-critique");
|
|
48503
49499
|
}
|
|
48504
49500
|
function snapshotPath(projectRoot, slug) {
|
|
48505
|
-
return
|
|
49501
|
+
return path31.join(critiqueCacheDir(projectRoot), `${slug}.json`);
|
|
48506
49502
|
}
|
|
48507
49503
|
async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
48508
49504
|
await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
|
|
@@ -48514,8 +49510,8 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
|
48514
49510
|
}
|
|
48515
49511
|
|
|
48516
49512
|
// src/commands/landing/source-version.ts
|
|
48517
|
-
import { readFile as
|
|
48518
|
-
import
|
|
49513
|
+
import { readdir as readdir11, readFile as readFile25, stat as stat7 } from "fs/promises";
|
|
49514
|
+
import path32 from "path";
|
|
48519
49515
|
var CRITIQUED_ROOTS = ["src/pages/", "src/components/"];
|
|
48520
49516
|
async function landingSourceRelPaths(root, slug) {
|
|
48521
49517
|
const files = await landingGraphFiles(root, slug);
|
|
@@ -48524,7 +49520,7 @@ async function landingSourceRelPaths(root, slug) {
|
|
|
48524
49520
|
async function readLandingSources(root, slug) {
|
|
48525
49521
|
const rel = await landingSourceRelPaths(root, slug);
|
|
48526
49522
|
const out = [];
|
|
48527
|
-
for (const r of rel) out.push({ path: r, text: await
|
|
49523
|
+
for (const r of rel) out.push({ path: r, text: await readFile25(path32.join(root, r), "utf8") });
|
|
48528
49524
|
return out;
|
|
48529
49525
|
}
|
|
48530
49526
|
async function computeLandingSourceSha(root, slug) {
|
|
@@ -48533,7 +49529,7 @@ async function computeLandingSourceSha(root, slug) {
|
|
|
48533
49529
|
for (const r of rel) {
|
|
48534
49530
|
let bytes;
|
|
48535
49531
|
try {
|
|
48536
|
-
bytes = await
|
|
49532
|
+
bytes = await readFile25(path32.join(root, r));
|
|
48537
49533
|
} catch {
|
|
48538
49534
|
bytes = Buffer.alloc(0);
|
|
48539
49535
|
}
|
|
@@ -48541,6 +49537,47 @@ async function computeLandingSourceSha(root, slug) {
|
|
|
48541
49537
|
}
|
|
48542
49538
|
return sha256Hex(Buffer.concat(parts));
|
|
48543
49539
|
}
|
|
49540
|
+
async function computeLegacyLandingSourceSha(landingDir) {
|
|
49541
|
+
const rel = [];
|
|
49542
|
+
if (await isFile2(path32.join(landingDir, "index.astro"))) rel.push("index.astro");
|
|
49543
|
+
for (const abs of await walkAstro(path32.join(landingDir, "_components"))) {
|
|
49544
|
+
rel.push(path32.relative(landingDir, abs).split(path32.sep).join("/"));
|
|
49545
|
+
}
|
|
49546
|
+
rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
49547
|
+
const parts = [];
|
|
49548
|
+
for (const r of rel) {
|
|
49549
|
+
let bytes;
|
|
49550
|
+
try {
|
|
49551
|
+
bytes = await readFile25(path32.join(landingDir, r));
|
|
49552
|
+
} catch {
|
|
49553
|
+
bytes = Buffer.alloc(0);
|
|
49554
|
+
}
|
|
49555
|
+
parts.push(Buffer.from(`${r}\0${bytes.length}\0`), bytes);
|
|
49556
|
+
}
|
|
49557
|
+
return sha256Hex(Buffer.concat(parts));
|
|
49558
|
+
}
|
|
49559
|
+
async function isFile2(p) {
|
|
49560
|
+
try {
|
|
49561
|
+
return (await stat7(p)).isFile();
|
|
49562
|
+
} catch {
|
|
49563
|
+
return false;
|
|
49564
|
+
}
|
|
49565
|
+
}
|
|
49566
|
+
async function walkAstro(dir) {
|
|
49567
|
+
let entries;
|
|
49568
|
+
try {
|
|
49569
|
+
entries = await readdir11(dir, { withFileTypes: true });
|
|
49570
|
+
} catch {
|
|
49571
|
+
return [];
|
|
49572
|
+
}
|
|
49573
|
+
const out = [];
|
|
49574
|
+
for (const entry of entries) {
|
|
49575
|
+
const abs = path32.join(dir, entry.name);
|
|
49576
|
+
if (entry.isDirectory()) out.push(...await walkAstro(abs));
|
|
49577
|
+
else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
|
|
49578
|
+
}
|
|
49579
|
+
return out;
|
|
49580
|
+
}
|
|
48544
49581
|
|
|
48545
49582
|
// src/commands/landing/critique.ts
|
|
48546
49583
|
registerSchema({
|
|
@@ -48596,7 +49633,7 @@ var critiqueCommand2 = defineCommand164({
|
|
|
48596
49633
|
{ availableSlugs: await listLandingSlugs(projectRoot) }
|
|
48597
49634
|
);
|
|
48598
49635
|
}
|
|
48599
|
-
if (!await isDir2(
|
|
49636
|
+
if (!await isDir2(path33.resolve(projectRoot, "src", "pages", slug))) {
|
|
48600
49637
|
fail6("NOT_FOUND", `No landing at src/pages/${slug}/`, {
|
|
48601
49638
|
availableSlugs: await listLandingSlugs(projectRoot)
|
|
48602
49639
|
});
|
|
@@ -48636,15 +49673,17 @@ var critiqueCommand2 = defineCommand164({
|
|
|
48636
49673
|
}
|
|
48637
49674
|
});
|
|
48638
49675
|
async function critiqueOne(projectRoot, slug, brand, competitors) {
|
|
48639
|
-
const [sources, sourceSha] = await Promise.all([
|
|
49676
|
+
const [sources, compositionSha, sourceSha] = await Promise.all([
|
|
48640
49677
|
readLandingSources(projectRoot, slug),
|
|
48641
|
-
computeLandingSourceSha(projectRoot, slug)
|
|
49678
|
+
computeLandingSourceSha(projectRoot, slug),
|
|
49679
|
+
computeLegacyLandingSourceSha(path33.resolve(projectRoot, "src", "pages", slug))
|
|
48642
49680
|
]);
|
|
48643
49681
|
const report = critiqueLanding({ slug, sources, brand, competitors });
|
|
48644
49682
|
let snapshotFailed = false;
|
|
48645
49683
|
try {
|
|
48646
49684
|
await writeCritiqueSnapshot(projectRoot, {
|
|
48647
49685
|
slug,
|
|
49686
|
+
compositionSha,
|
|
48648
49687
|
sourceSha,
|
|
48649
49688
|
criticVersion: CRITIC_VERSION,
|
|
48650
49689
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -48657,18 +49696,18 @@ async function critiqueOne(projectRoot, slug, brand, competitors) {
|
|
|
48657
49696
|
return { slug, report, snapshotFailed };
|
|
48658
49697
|
}
|
|
48659
49698
|
async function readCompetitorNames(projectRoot) {
|
|
48660
|
-
const dir =
|
|
49699
|
+
const dir = path33.join(projectRoot, "src", "content", "competitors");
|
|
48661
49700
|
let entries;
|
|
48662
49701
|
try {
|
|
48663
|
-
entries = (await
|
|
49702
|
+
entries = (await readdir12(dir)).filter((f) => f.endsWith(".md"));
|
|
48664
49703
|
} catch {
|
|
48665
49704
|
return [];
|
|
48666
49705
|
}
|
|
48667
49706
|
const names = [];
|
|
48668
49707
|
for (const entry of entries) {
|
|
48669
|
-
names.push(
|
|
49708
|
+
names.push(path33.basename(entry, ".md").replace(/[-_]+/g, " "));
|
|
48670
49709
|
try {
|
|
48671
|
-
const head = (await
|
|
49710
|
+
const head = (await readFile26(path33.join(dir, entry), "utf8")).slice(0, 2e3);
|
|
48672
49711
|
const titled = /^\s*(?:title|name)\s*:\s*["']?([^"'\n]+)["']?\s*$/im.exec(head);
|
|
48673
49712
|
if (titled?.[1]) names.push(titled[1].trim());
|
|
48674
49713
|
} catch {
|
|
@@ -48678,7 +49717,7 @@ async function readCompetitorNames(projectRoot) {
|
|
|
48678
49717
|
}
|
|
48679
49718
|
async function listLandingSlugs(projectRoot) {
|
|
48680
49719
|
try {
|
|
48681
|
-
const entries = await
|
|
49720
|
+
const entries = await readdir12(path33.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
48682
49721
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
48683
49722
|
} catch {
|
|
48684
49723
|
return [];
|
|
@@ -48697,7 +49736,7 @@ function present(f) {
|
|
|
48697
49736
|
}
|
|
48698
49737
|
async function isDir2(p) {
|
|
48699
49738
|
try {
|
|
48700
|
-
return (await
|
|
49739
|
+
return (await stat8(p)).isDirectory();
|
|
48701
49740
|
} catch {
|
|
48702
49741
|
return false;
|
|
48703
49742
|
}
|
|
@@ -48830,7 +49869,7 @@ var addCommand = defineCommand165({
|
|
|
48830
49869
|
|
|
48831
49870
|
// src/commands/landing/inspiration/code.ts
|
|
48832
49871
|
import { mkdir as mkdir10, writeFile as writeFile14 } from "fs/promises";
|
|
48833
|
-
import
|
|
49872
|
+
import path34 from "path";
|
|
48834
49873
|
import { defineCommand as defineCommand166 } from "citty";
|
|
48835
49874
|
registerSchema({
|
|
48836
49875
|
command: "landing.inspiration.code",
|
|
@@ -48853,9 +49892,9 @@ var codeCommand = defineCommand166({
|
|
|
48853
49892
|
try {
|
|
48854
49893
|
const id = args.id;
|
|
48855
49894
|
const data = await apiGet("/api/landing-inspiration/section-code", { id });
|
|
48856
|
-
const dir =
|
|
49895
|
+
const dir = path34.join(process.cwd(), ".baker", "inspiration", id);
|
|
48857
49896
|
await mkdir10(dir, { recursive: true });
|
|
48858
|
-
const file =
|
|
49897
|
+
const file = path34.join(dir, "section.html");
|
|
48859
49898
|
await writeFile14(file, data.html);
|
|
48860
49899
|
const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
48861
49900
|
const fidelity = fidelityHint(data.fidelity);
|
|
@@ -48865,7 +49904,7 @@ var codeCommand = defineCommand166({
|
|
|
48865
49904
|
ok: true,
|
|
48866
49905
|
data: {
|
|
48867
49906
|
id,
|
|
48868
|
-
file:
|
|
49907
|
+
file: path34.relative(process.cwd(), file),
|
|
48869
49908
|
bytes: data.html.length,
|
|
48870
49909
|
fidelity: data.fidelity,
|
|
48871
49910
|
reproduction_notes: data.reproductionNotes,
|
|
@@ -49297,7 +50336,7 @@ function classifyCaptureFailure(error) {
|
|
|
49297
50336
|
|
|
49298
50337
|
// src/engine/landing-library/run.ts
|
|
49299
50338
|
import { mkdir as mkdir11, writeFile as writeFile16 } from "fs/promises";
|
|
49300
|
-
import
|
|
50339
|
+
import path36 from "path";
|
|
49301
50340
|
|
|
49302
50341
|
// ../proxy/src/preflight.ts
|
|
49303
50342
|
import http from "http";
|
|
@@ -49436,7 +50475,7 @@ var inPageCollectUsedCss = (selector) => {
|
|
|
49436
50475
|
}
|
|
49437
50476
|
return false;
|
|
49438
50477
|
};
|
|
49439
|
-
const
|
|
50478
|
+
const walk2 = (rules, sink) => {
|
|
49440
50479
|
for (const rule of Array.from(rules)) {
|
|
49441
50480
|
totalRules++;
|
|
49442
50481
|
if (rule instanceof CSSStyleRule) {
|
|
@@ -49457,7 +50496,7 @@ var inPageCollectUsedCss = (selector) => {
|
|
|
49457
50496
|
const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
|
|
49458
50497
|
if (grouping) {
|
|
49459
50498
|
const inner = [];
|
|
49460
|
-
|
|
50499
|
+
walk2(rule.cssRules, inner);
|
|
49461
50500
|
if (inner.length === 0) continue;
|
|
49462
50501
|
const condition = rule.conditionText ?? "";
|
|
49463
50502
|
const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
|
|
@@ -49471,7 +50510,7 @@ ${inner.join("\n")}
|
|
|
49471
50510
|
const owner = sheet.ownerNode;
|
|
49472
50511
|
if (owner?.hasAttribute?.("data-baker-freeze")) continue;
|
|
49473
50512
|
try {
|
|
49474
|
-
|
|
50513
|
+
walk2(sheet.cssRules, kept);
|
|
49475
50514
|
} catch {
|
|
49476
50515
|
if (sheet.href) unreadableHrefs.push(sheet.href);
|
|
49477
50516
|
}
|
|
@@ -49922,7 +50961,7 @@ var inPageCollectMotion = (selector) => {
|
|
|
49922
50961
|
else if (infinite) loop.push(effect);
|
|
49923
50962
|
else entrance.push(effect);
|
|
49924
50963
|
};
|
|
49925
|
-
const
|
|
50964
|
+
const walk2 = (rules, insideScrollTimeline) => {
|
|
49926
50965
|
for (const rule of Array.from(rules)) {
|
|
49927
50966
|
if (rule instanceof CSSKeyframesRule) {
|
|
49928
50967
|
keyframesByName.set(rule.name, rule.cssText);
|
|
@@ -49940,17 +50979,17 @@ var inPageCollectMotion = (selector) => {
|
|
|
49940
50979
|
respectsReducedMotion = true;
|
|
49941
50980
|
continue;
|
|
49942
50981
|
}
|
|
49943
|
-
|
|
50982
|
+
walk2(rule.cssRules, insideScrollTimeline);
|
|
49944
50983
|
continue;
|
|
49945
50984
|
}
|
|
49946
50985
|
const grouping = rule;
|
|
49947
|
-
if (grouping.cssRules)
|
|
50986
|
+
if (grouping.cssRules) walk2(grouping.cssRules, insideScrollTimeline);
|
|
49948
50987
|
}
|
|
49949
50988
|
};
|
|
49950
50989
|
for (const sheet of Array.from(document.styleSheets)) {
|
|
49951
50990
|
if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
|
|
49952
50991
|
try {
|
|
49953
|
-
|
|
50992
|
+
walk2(sheet.cssRules, false);
|
|
49954
50993
|
} catch {
|
|
49955
50994
|
}
|
|
49956
50995
|
}
|
|
@@ -50713,9 +51752,9 @@ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
|
|
|
50713
51752
|
|
|
50714
51753
|
// src/engine/landing-library/report.ts
|
|
50715
51754
|
import { writeFile as writeFile15 } from "fs/promises";
|
|
50716
|
-
import
|
|
51755
|
+
import path35 from "path";
|
|
50717
51756
|
async function writeCaptureReport(manifest, outDir) {
|
|
50718
|
-
const file =
|
|
51757
|
+
const file = path35.join(outDir, "report.html");
|
|
50719
51758
|
await writeFile15(file, renderReport(manifest));
|
|
50720
51759
|
return file;
|
|
50721
51760
|
}
|
|
@@ -50894,32 +51933,32 @@ async function reproducePage(args) {
|
|
|
50894
51933
|
const { browser, page, outDir, pageUrl, livePageShot } = args;
|
|
50895
51934
|
const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
|
|
50896
51935
|
if (!built) return { bundle: null, fidelity: null };
|
|
50897
|
-
await writeFile16(
|
|
51936
|
+
await writeFile16(path36.join(outDir, "page.html"), built.html);
|
|
50898
51937
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
|
|
50899
51938
|
wholePage: true,
|
|
50900
51939
|
timeoutMs: 6e4
|
|
50901
51940
|
});
|
|
50902
51941
|
if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
|
|
50903
|
-
await writeFile16(
|
|
51942
|
+
await writeFile16(path36.join(outDir, "page-rendered.png"), rendered);
|
|
50904
51943
|
const { score, note } = await scoreFidelity(livePageShot, rendered);
|
|
50905
51944
|
return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
|
|
50906
51945
|
}
|
|
50907
51946
|
async function captureOneSection(args) {
|
|
50908
51947
|
const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
|
|
50909
|
-
const dir =
|
|
51948
|
+
const dir = path36.join(sectionsDir, String(candidate.index).padStart(2, "0"));
|
|
50910
51949
|
await mkdir11(dir, { recursive: true });
|
|
50911
51950
|
const desktop = await captureSection(page, candidate);
|
|
50912
|
-
if (desktop) await writeFile16(
|
|
51951
|
+
if (desktop) await writeFile16(path36.join(dir, "desktop.png"), desktop);
|
|
50913
51952
|
const visualHash = desktop ? await perceptualHash(desktop) : null;
|
|
50914
51953
|
const motion = await collectMotion(page, candidate.selector);
|
|
50915
51954
|
const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
|
|
50916
51955
|
let fidelity = null;
|
|
50917
51956
|
let fidelityNote;
|
|
50918
51957
|
if (built) {
|
|
50919
|
-
await writeFile16(
|
|
51958
|
+
await writeFile16(path36.join(dir, "section.html"), built.html);
|
|
50920
51959
|
const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
|
|
50921
51960
|
if (rendered && desktop) {
|
|
50922
|
-
await writeFile16(
|
|
51961
|
+
await writeFile16(path36.join(dir, "section-rendered.png"), rendered);
|
|
50923
51962
|
const result = await scoreFidelity(desktop, rendered);
|
|
50924
51963
|
fidelity = result.score;
|
|
50925
51964
|
fidelityNote = result.note;
|
|
@@ -50927,9 +51966,9 @@ async function captureOneSection(args) {
|
|
|
50927
51966
|
}
|
|
50928
51967
|
return {
|
|
50929
51968
|
...candidate,
|
|
50930
|
-
desktopShot: desktop ?
|
|
51969
|
+
desktopShot: desktop ? path36.relative(outDir, path36.join(dir, "desktop.png")) : null,
|
|
50931
51970
|
mobileShot: null,
|
|
50932
|
-
bundle: built ?
|
|
51971
|
+
bundle: built ? path36.relative(outDir, path36.join(dir, "section.html")) : null,
|
|
50933
51972
|
fidelity,
|
|
50934
51973
|
...fidelityNote ? { fidelityNote } : {},
|
|
50935
51974
|
...built ? { cssStats: built.stats } : {},
|
|
@@ -50948,9 +51987,9 @@ async function captureMobileShots(args) {
|
|
|
50948
51987
|
for (const section of sections) {
|
|
50949
51988
|
const shot = await captureSectionOnMobile(mobile.page, section);
|
|
50950
51989
|
if (!shot) continue;
|
|
50951
|
-
const file =
|
|
51990
|
+
const file = path36.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
|
|
50952
51991
|
await writeFile16(file, shot);
|
|
50953
|
-
section.mobileShot =
|
|
51992
|
+
section.mobileShot = path36.relative(outDir, file);
|
|
50954
51993
|
}
|
|
50955
51994
|
} finally {
|
|
50956
51995
|
await mobile.context.close();
|
|
@@ -50965,10 +52004,10 @@ async function captureMotionTakes(args) {
|
|
|
50965
52004
|
const filmOne = async (section) => {
|
|
50966
52005
|
const take = await captureMotionTake(browser, pageUrl, section.selector).catch(() => null);
|
|
50967
52006
|
if (!take) return;
|
|
50968
|
-
const dir =
|
|
50969
|
-
const file =
|
|
52007
|
+
const dir = path36.join(sectionsDir, String(section.index).padStart(2, "0"));
|
|
52008
|
+
const file = path36.join(dir, "motion-filmstrip.png");
|
|
50970
52009
|
await writeFile16(file, take.filmstrip);
|
|
50971
|
-
section.motionFilmstrip =
|
|
52010
|
+
section.motionFilmstrip = path36.relative(outDir, file);
|
|
50972
52011
|
log(` [${section.index}] ${section.motion.summary}`);
|
|
50973
52012
|
};
|
|
50974
52013
|
const queue = [...moving];
|
|
@@ -51025,7 +52064,7 @@ async function captureAlternateViews(args) {
|
|
|
51025
52064
|
async function reproduceWholePage(args) {
|
|
51026
52065
|
const { browser, page, outDir, pageUrl, withCode, log } = args;
|
|
51027
52066
|
const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
|
|
51028
|
-
if (fullPage) await writeFile16(
|
|
52067
|
+
if (fullPage) await writeFile16(path36.join(outDir, "full-page.png"), fullPage);
|
|
51029
52068
|
if (!withCode) return { bundle: null, fidelity: null };
|
|
51030
52069
|
const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
|
|
51031
52070
|
log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
|
|
@@ -51096,7 +52135,7 @@ async function openViaLadder(args) {
|
|
|
51096
52135
|
async function scrapeLanding(options) {
|
|
51097
52136
|
const timeoutMs = options.timeoutMs ?? 45e3;
|
|
51098
52137
|
const log = options.onProgress ?? (() => void 0);
|
|
51099
|
-
const sectionsDir =
|
|
52138
|
+
const sectionsDir = path36.join(options.outDir, "sections");
|
|
51100
52139
|
const nonPublic = refuseNonPublicUrl(options.url);
|
|
51101
52140
|
if (nonPublic) {
|
|
51102
52141
|
throw new BlockedPageError({
|
|
@@ -51158,7 +52197,7 @@ async function scrapeLanding(options) {
|
|
|
51158
52197
|
security: prepared.security,
|
|
51159
52198
|
captureTier: tier
|
|
51160
52199
|
};
|
|
51161
|
-
await writeFile16(
|
|
52200
|
+
await writeFile16(path36.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
51162
52201
|
`);
|
|
51163
52202
|
if (options.report !== false) {
|
|
51164
52203
|
const reportPath = await writeCaptureReport(manifest, options.outDir);
|
|
@@ -51173,28 +52212,28 @@ async function scrapeLanding(options) {
|
|
|
51173
52212
|
|
|
51174
52213
|
// src/commands/landing/inspiration/captureOut.ts
|
|
51175
52214
|
import { existsSync as existsSync9 } from "fs";
|
|
51176
|
-
import
|
|
52215
|
+
import path37 from "path";
|
|
51177
52216
|
var SCRATCH_DIR = ".baker";
|
|
51178
52217
|
function isWithin(parent, target) {
|
|
51179
|
-
const relative =
|
|
51180
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
52218
|
+
const relative = path37.relative(parent, target);
|
|
52219
|
+
return relative === "" || !relative.startsWith("..") && !path37.isAbsolute(relative);
|
|
51181
52220
|
}
|
|
51182
52221
|
function findRepoRoot(from) {
|
|
51183
|
-
let dir =
|
|
52222
|
+
let dir = path37.resolve(from);
|
|
51184
52223
|
for (; ; ) {
|
|
51185
|
-
if (existsSync9(
|
|
51186
|
-
const parent =
|
|
52224
|
+
if (existsSync9(path37.join(dir, ".git"))) return dir;
|
|
52225
|
+
const parent = path37.dirname(dir);
|
|
51187
52226
|
if (parent === dir) return null;
|
|
51188
52227
|
dir = parent;
|
|
51189
52228
|
}
|
|
51190
52229
|
}
|
|
51191
52230
|
function checkCaptureOut(out, options) {
|
|
51192
52231
|
const { cwd, repoRoot } = options;
|
|
51193
|
-
const resolved =
|
|
52232
|
+
const resolved = path37.resolve(cwd, out);
|
|
51194
52233
|
if (repoRoot === null || !isWithin(repoRoot, resolved)) return { ok: true };
|
|
51195
|
-
const scratch =
|
|
52234
|
+
const scratch = path37.join(repoRoot, SCRATCH_DIR);
|
|
51196
52235
|
if (isWithin(scratch, resolved)) return { ok: true };
|
|
51197
|
-
const suggestion =
|
|
52236
|
+
const suggestion = path37.posix.join(SCRATCH_DIR, "teardowns", path37.basename(resolved) || "capture");
|
|
51198
52237
|
return {
|
|
51199
52238
|
ok: false,
|
|
51200
52239
|
error: {
|
|
@@ -51368,12 +52407,12 @@ var scrapeCommand = defineCommand169({
|
|
|
51368
52407
|
});
|
|
51369
52408
|
|
|
51370
52409
|
// src/commands/landing/inspiration/search.ts
|
|
51371
|
-
import
|
|
52410
|
+
import path39 from "path";
|
|
51372
52411
|
import { defineCommand as defineCommand170 } from "citty";
|
|
51373
52412
|
|
|
51374
52413
|
// src/commands/landing/inspiration/shot.ts
|
|
51375
52414
|
import { mkdir as mkdir12, writeFile as writeFile17 } from "fs/promises";
|
|
51376
|
-
import
|
|
52415
|
+
import path38 from "path";
|
|
51377
52416
|
import sharp6 from "sharp";
|
|
51378
52417
|
var READABLE_SHOT = {
|
|
51379
52418
|
maxWidth: 1440,
|
|
@@ -51399,9 +52438,9 @@ async function downloadReadableShot(url, file) {
|
|
|
51399
52438
|
const response = await fetch(url);
|
|
51400
52439
|
if (!response.ok) return null;
|
|
51401
52440
|
const shot = await toReadableShot(Buffer.from(await response.arrayBuffer()));
|
|
51402
|
-
await mkdir12(
|
|
52441
|
+
await mkdir12(path38.dirname(file), { recursive: true });
|
|
51403
52442
|
await writeFile17(file, shot);
|
|
51404
|
-
return
|
|
52443
|
+
return path38.relative(process.cwd(), file);
|
|
51405
52444
|
} catch {
|
|
51406
52445
|
return null;
|
|
51407
52446
|
}
|
|
@@ -51493,13 +52532,13 @@ function buildSearchBody(args) {
|
|
|
51493
52532
|
return body;
|
|
51494
52533
|
}
|
|
51495
52534
|
async function downloadShots(results) {
|
|
51496
|
-
const dir =
|
|
52535
|
+
const dir = path39.join(process.cwd(), ".baker", "inspiration");
|
|
51497
52536
|
const saved = /* @__PURE__ */ new Map();
|
|
51498
52537
|
await Promise.all(
|
|
51499
52538
|
results.map(async (result) => {
|
|
51500
52539
|
const file = await downloadReadableShot(
|
|
51501
52540
|
result.desktopShotUrl,
|
|
51502
|
-
|
|
52541
|
+
path39.join(dir, `${result.id}.${READABLE_SHOT.extension}`)
|
|
51503
52542
|
);
|
|
51504
52543
|
if (file) saved.set(result.id, file);
|
|
51505
52544
|
})
|
|
@@ -51727,7 +52766,7 @@ var sequencesCommand = defineCommand171({
|
|
|
51727
52766
|
});
|
|
51728
52767
|
|
|
51729
52768
|
// src/commands/landing/inspiration/view.ts
|
|
51730
|
-
import
|
|
52769
|
+
import path40 from "path";
|
|
51731
52770
|
import { defineCommand as defineCommand172 } from "citty";
|
|
51732
52771
|
registerSchema({
|
|
51733
52772
|
command: "landing.inspiration.view",
|
|
@@ -51760,12 +52799,12 @@ var viewCommand2 = defineCommand172({
|
|
|
51760
52799
|
const id = args.id;
|
|
51761
52800
|
const data = await apiGet("/api/landing-inspiration/section", { id });
|
|
51762
52801
|
const section = data.section;
|
|
51763
|
-
const dir =
|
|
52802
|
+
const dir = path40.join(process.cwd(), ".baker", "inspiration", id);
|
|
51764
52803
|
const ext = READABLE_SHOT.extension;
|
|
51765
52804
|
const [desktop, mobile, filmstrip] = await Promise.all([
|
|
51766
|
-
downloadReadableShot(section.desktopShotUrl,
|
|
51767
|
-
downloadReadableShot(section.mobileShotUrl,
|
|
51768
|
-
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}`))
|
|
51769
52808
|
]);
|
|
51770
52809
|
const full = args.full;
|
|
51771
52810
|
const hints2 = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
|
|
@@ -51871,8 +52910,8 @@ Full guide: __tooling__/docs/tools/baker/landing.md`
|
|
|
51871
52910
|
|
|
51872
52911
|
// src/commands/landing/variant.ts
|
|
51873
52912
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
51874
|
-
import { mkdir as mkdir13, readdir as
|
|
51875
|
-
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";
|
|
51876
52915
|
import { defineCommand as defineCommand174 } from "citty";
|
|
51877
52916
|
registerSchema({
|
|
51878
52917
|
command: "landing.variant",
|
|
@@ -51899,7 +52938,7 @@ registerSchema({
|
|
|
51899
52938
|
var SLUG_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
51900
52939
|
async function readInternalId(definitionPath) {
|
|
51901
52940
|
try {
|
|
51902
|
-
const text2 = await
|
|
52941
|
+
const text2 = await readFile27(definitionPath, "utf8");
|
|
51903
52942
|
return /^internalId:\s*"?([^"\s]+)"?\s*$/m.exec(text2)?.[1] ?? null;
|
|
51904
52943
|
} catch {
|
|
51905
52944
|
return null;
|
|
@@ -51908,7 +52947,7 @@ async function readInternalId(definitionPath) {
|
|
|
51908
52947
|
async function archivedVariantNumbers(root, internalId) {
|
|
51909
52948
|
if (!internalId) return [];
|
|
51910
52949
|
try {
|
|
51911
|
-
const entries = await
|
|
52950
|
+
const entries = await readdir13(path41.resolve(root, "src/_variants", internalId), { withFileTypes: true });
|
|
51912
52951
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => Number(entry.name)).filter((n) => Number.isInteger(n) && n > 0);
|
|
51913
52952
|
} catch {
|
|
51914
52953
|
return [];
|
|
@@ -51923,14 +52962,14 @@ function fail7(code, message, fix) {
|
|
|
51923
52962
|
}
|
|
51924
52963
|
async function isDir3(p) {
|
|
51925
52964
|
try {
|
|
51926
|
-
return (await
|
|
52965
|
+
return (await stat9(p)).isDirectory();
|
|
51927
52966
|
} catch {
|
|
51928
52967
|
return false;
|
|
51929
52968
|
}
|
|
51930
52969
|
}
|
|
51931
52970
|
async function exists(p) {
|
|
51932
52971
|
try {
|
|
51933
|
-
await
|
|
52972
|
+
await stat9(p);
|
|
51934
52973
|
return true;
|
|
51935
52974
|
} catch {
|
|
51936
52975
|
return false;
|
|
@@ -51938,7 +52977,7 @@ async function exists(p) {
|
|
|
51938
52977
|
}
|
|
51939
52978
|
async function listLandingSlugs2(root) {
|
|
51940
52979
|
try {
|
|
51941
|
-
const entries = await
|
|
52980
|
+
const entries = await readdir13(path41.resolve(root, "src", "pages"), { withFileTypes: true });
|
|
51942
52981
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
51943
52982
|
} catch {
|
|
51944
52983
|
return [];
|
|
@@ -51947,14 +52986,14 @@ async function listLandingSlugs2(root) {
|
|
|
51947
52986
|
async function listComponents(componentsDir, prefix = "") {
|
|
51948
52987
|
let entries;
|
|
51949
52988
|
try {
|
|
51950
|
-
entries = await
|
|
52989
|
+
entries = await readdir13(componentsDir, { withFileTypes: true });
|
|
51951
52990
|
} catch {
|
|
51952
52991
|
return [];
|
|
51953
52992
|
}
|
|
51954
52993
|
const out = [];
|
|
51955
52994
|
for (const entry of entries) {
|
|
51956
52995
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
51957
|
-
if (entry.isDirectory()) out.push(...await listComponents(
|
|
52996
|
+
if (entry.isDirectory()) out.push(...await listComponents(path41.join(componentsDir, entry.name), rel));
|
|
51958
52997
|
else if (entry.name.endsWith(".astro")) out.push(rel);
|
|
51959
52998
|
}
|
|
51960
52999
|
return out.sort();
|
|
@@ -51968,27 +53007,39 @@ function resolveForkName(requested, available) {
|
|
|
51968
53007
|
const matches = available.filter((a) => basename4(a) === cleaned || basename4(a) === `${cleaned}.astro`);
|
|
51969
53008
|
return matches.length === 1 ? matches[0] ?? null : null;
|
|
51970
53009
|
}
|
|
51971
|
-
function parseForks(fork) {
|
|
51972
|
-
const
|
|
51973
|
-
const names = list.filter((s) => typeof s === "string").flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
53010
|
+
function parseForks(fork, rawArgs) {
|
|
53011
|
+
const names = repeatedValues(rawArgs, "fork", fork).flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
51974
53012
|
return [...new Set(names)];
|
|
51975
53013
|
}
|
|
53014
|
+
function pageInTest(status, page) {
|
|
53015
|
+
for (const test of status.experiments) {
|
|
53016
|
+
if (test.landingSlug !== page) continue;
|
|
53017
|
+
if (test.status !== "finished") {
|
|
53018
|
+
return `${page} is already in a test (${test.experimentId}, against ${test.variantSlug}, ${test.status}). One page has one test at a time \u2014 read it with \`baker experiment status --landing ${page}\`, and end it with \`baker experiment finish\` before building the next version.`;
|
|
53019
|
+
}
|
|
53020
|
+
if (test.fold === "owed" || test.fold === "landing") {
|
|
53021
|
+
return `The last test on ${page} was won by ${test.variantSlug}, and the page is still being updated to match \u2014 run \`baker experiment fold\` and publish before building the next version.`;
|
|
53022
|
+
}
|
|
53023
|
+
}
|
|
53024
|
+
return null;
|
|
53025
|
+
}
|
|
51976
53026
|
function buildVariantDefinition(controlDefinition, opts) {
|
|
51977
|
-
|
|
53027
|
+
const frontmatter = controlDefinition.match(/^---\n[\s\S]*?\n---/)?.[0] ?? "---\n---";
|
|
53028
|
+
let out = frontmatter;
|
|
51978
53029
|
out = out.replace(/^internalId:.*$/m, `internalId: "${opts.internalId}"`);
|
|
51979
53030
|
out = out.replace(/^internalTitle:\s*(.*)$/m, (_m, title) => `internalTitle: ${title.trim()} (variant)`);
|
|
51980
53031
|
const shared = opts.forks.length > 0 ? opts.forks.join(", ") : "nothing yet";
|
|
51981
53032
|
const note = [
|
|
51982
53033
|
"",
|
|
51983
|
-
|
|
53034
|
+
`# Version ${variantNumberOf(opts.variantSlug) ?? "?"} of \`${opts.controlSlug}\` \u2014 for an A/B test`,
|
|
51984
53035
|
"",
|
|
51985
|
-
`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.`,
|
|
51986
53037
|
"",
|
|
51987
|
-
`**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/\`.`,
|
|
51988
53039
|
...opts.because ? ["", `**Because:** ${opts.because}`] : [],
|
|
51989
53040
|
...opts.change ? ["", `**We changed:** ${opts.change}`] : [],
|
|
51990
53041
|
"",
|
|
51991
|
-
`Run \`baker experiment start --landing ${opts.controlSlug} --variant ${opts.variantSlug} --because "\u2026" --change "\u2026"\` to begin, and read the result with \`baker experiment status
|
|
53042
|
+
`Run \`baker experiment start --landing ${opts.controlSlug} --variant ${opts.variantSlug} --because "\u2026" --change "\u2026"\` to begin, and read the result with \`baker experiment status\`. When the test ends, \`baker experiment fold\` moves the version that won into the page and removes this folder.`,
|
|
51992
53043
|
""
|
|
51993
53044
|
].join("\n");
|
|
51994
53045
|
return `${out.trimEnd()}
|
|
@@ -52009,16 +53060,16 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
52009
53060
|
`"${controlSlug}" is already a variant of "${variantOfSlug(controlSlug)}". Make the next variant of the page itself: \`baker landing variant ${variantOfSlug(controlSlug)}\`.`
|
|
52010
53061
|
);
|
|
52011
53062
|
}
|
|
52012
|
-
const controlDir =
|
|
53063
|
+
const controlDir = path41.resolve(projectRoot, "src", "pages", controlSlug);
|
|
52013
53064
|
if (!await isDir3(controlDir)) {
|
|
52014
53065
|
fail7("NOT_FOUND", `No page at src/pages/${controlSlug}/`, {
|
|
52015
53066
|
availableSlugs: await listLandingSlugs2(projectRoot)
|
|
52016
53067
|
});
|
|
52017
53068
|
}
|
|
52018
|
-
if (!await exists(
|
|
53069
|
+
if (!await exists(path41.join(controlDir, "index.astro"))) {
|
|
52019
53070
|
fail7("NOT_FOUND", `src/pages/${controlSlug}/index.astro is missing, so there is no page to make a variant of.`);
|
|
52020
53071
|
}
|
|
52021
|
-
const internalId = await readInternalId(
|
|
53072
|
+
const internalId = await readInternalId(path41.join(controlDir, "_definition.md"));
|
|
52022
53073
|
const variantSlugValue = variantSlug(
|
|
52023
53074
|
controlSlug,
|
|
52024
53075
|
nextVariantNumber(
|
|
@@ -52027,10 +53078,10 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
52027
53078
|
await archivedVariantNumbers(projectRoot, internalId)
|
|
52028
53079
|
)
|
|
52029
53080
|
);
|
|
52030
|
-
const variantDir =
|
|
52031
|
-
const available = await listComponents(
|
|
53081
|
+
const variantDir = path41.resolve(projectRoot, "src", "pages", variantSlugValue);
|
|
53082
|
+
const available = await listComponents(path41.join(controlDir, "_components"));
|
|
52032
53083
|
const forks = [];
|
|
52033
|
-
for (const name of parseForks(opts.fork)) {
|
|
53084
|
+
for (const name of parseForks(opts.fork, opts.rawArgs)) {
|
|
52034
53085
|
const resolved = resolveForkName(name, available);
|
|
52035
53086
|
if (resolved === null) {
|
|
52036
53087
|
fail7("NOT_FOUND", `"${name}" is not a component of ${controlSlug}.`, {
|
|
@@ -52044,26 +53095,26 @@ async function resolveTargets(projectRoot, opts) {
|
|
|
52044
53095
|
}
|
|
52045
53096
|
async function writeVariant(opts) {
|
|
52046
53097
|
const { controlDir, variantDir, controlSlug, variantSlug: variantSlug2, forks } = opts;
|
|
52047
|
-
const forkedTargets = new Set(forks.map((f) =>
|
|
53098
|
+
const forkedTargets = new Set(forks.map((f) => path41.join(controlDir, "_components", f)));
|
|
52048
53099
|
const written = [];
|
|
52049
|
-
const indexText = await
|
|
53100
|
+
const indexText = await readFile27(path41.join(controlDir, "index.astro"), "utf8");
|
|
52050
53101
|
await mkdir13(variantDir, { recursive: true });
|
|
52051
53102
|
await writeFile18(
|
|
52052
|
-
|
|
53103
|
+
path41.join(variantDir, "index.astro"),
|
|
52053
53104
|
repointFile(indexText, { fromDir: controlDir, toDir: variantDir, controlDir, variantDir, forkedTargets }),
|
|
52054
53105
|
"utf8"
|
|
52055
53106
|
);
|
|
52056
53107
|
written.push(`src/pages/${variantSlug2}/index.astro`);
|
|
52057
53108
|
for (const fork of forks) {
|
|
52058
|
-
const fromFile =
|
|
52059
|
-
const toFile =
|
|
52060
|
-
const text2 = await
|
|
52061
|
-
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 });
|
|
52062
53113
|
await writeFile18(
|
|
52063
53114
|
toFile,
|
|
52064
53115
|
repointFile(text2, {
|
|
52065
|
-
fromDir:
|
|
52066
|
-
toDir:
|
|
53116
|
+
fromDir: path41.dirname(fromFile),
|
|
53117
|
+
toDir: path41.dirname(toFile),
|
|
52067
53118
|
controlDir,
|
|
52068
53119
|
variantDir,
|
|
52069
53120
|
forkedTargets
|
|
@@ -52072,9 +53123,9 @@ async function writeVariant(opts) {
|
|
|
52072
53123
|
);
|
|
52073
53124
|
written.push(`src/pages/${variantSlug2}/_components/${fork}`);
|
|
52074
53125
|
}
|
|
52075
|
-
const controlDefinitionPath =
|
|
53126
|
+
const controlDefinitionPath = path41.join(controlDir, "_definition.md");
|
|
52076
53127
|
if (await exists(controlDefinitionPath)) {
|
|
52077
|
-
const definition = buildVariantDefinition(await
|
|
53128
|
+
const definition = buildVariantDefinition(await readFile27(controlDefinitionPath, "utf8"), {
|
|
52078
53129
|
internalId: randomUUID2().replace(/-/g, "").slice(0, 8),
|
|
52079
53130
|
variantSlug: variantSlug2,
|
|
52080
53131
|
controlSlug,
|
|
@@ -52082,11 +53133,11 @@ async function writeVariant(opts) {
|
|
|
52082
53133
|
...opts.because ? { because: opts.because } : {},
|
|
52083
53134
|
...opts.change ? { change: opts.change } : {}
|
|
52084
53135
|
});
|
|
52085
|
-
await writeFile18(
|
|
53136
|
+
await writeFile18(path41.join(variantDir, "_definition.md"), definition, "utf8");
|
|
52086
53137
|
written.push(`src/pages/${variantSlug2}/_definition.md`);
|
|
52087
53138
|
}
|
|
52088
|
-
await mkdir13(
|
|
52089
|
-
await writeFile18(
|
|
53139
|
+
await mkdir13(path41.join(variantDir, "_images"), { recursive: true });
|
|
53140
|
+
await writeFile18(path41.join(variantDir, "_images", ".gitkeep"), "", "utf8");
|
|
52090
53141
|
return written;
|
|
52091
53142
|
}
|
|
52092
53143
|
var variantCommand = defineCommand174({
|
|
@@ -52111,13 +53162,28 @@ var variantCommand = defineCommand174({
|
|
|
52111
53162
|
description: "What is different about this version, in the client's words \u2014 \u201Cput the booking form in the hero\u201D."
|
|
52112
53163
|
}
|
|
52113
53164
|
},
|
|
52114
|
-
async run({ args }) {
|
|
53165
|
+
async run({ args, rawArgs }) {
|
|
52115
53166
|
const projectRoot = process.cwd();
|
|
52116
53167
|
const controlSlug = String(args.page);
|
|
52117
53168
|
const { controlDir, variantDir, variantSlug: variantSlug2, available, forks } = await resolveTargets(projectRoot, {
|
|
52118
53169
|
controlSlug,
|
|
52119
|
-
fork: args.fork
|
|
53170
|
+
fork: args.fork,
|
|
53171
|
+
rawArgs
|
|
52120
53172
|
});
|
|
53173
|
+
let status;
|
|
53174
|
+
try {
|
|
53175
|
+
const response = await apiPost("/api/experiments/status", {
|
|
53176
|
+
landingSlug: controlSlug
|
|
53177
|
+
});
|
|
53178
|
+
status = response.data;
|
|
53179
|
+
} catch (error) {
|
|
53180
|
+
fail7(
|
|
53181
|
+
"UNAVAILABLE",
|
|
53182
|
+
`Could not check whether ${controlSlug} is already in a test (${error instanceof Error ? error.message : String(error)}). Nothing was written \u2014 run \`baker experiment status --landing ${controlSlug}\` and try again.`
|
|
53183
|
+
);
|
|
53184
|
+
}
|
|
53185
|
+
const taken = pageInTest(status, controlSlug);
|
|
53186
|
+
if (taken) fail7("CONFLICT", taken, { landing: controlSlug });
|
|
52121
53187
|
const written = await writeVariant({
|
|
52122
53188
|
controlDir,
|
|
52123
53189
|
variantDir,
|
|
@@ -54093,8 +55159,8 @@ var listCommand16 = defineCommand193({
|
|
|
54093
55159
|
});
|
|
54094
55160
|
|
|
54095
55161
|
// src/commands/scheduled-actions/templates.ts
|
|
54096
|
-
import { readFile as
|
|
54097
|
-
import
|
|
55162
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
55163
|
+
import path42 from "path";
|
|
54098
55164
|
import { defineCommand as defineCommand194 } from "citty";
|
|
54099
55165
|
registerSchema({
|
|
54100
55166
|
command: "scheduled-actions.templates",
|
|
@@ -54197,7 +55263,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
54197
55263
|
}
|
|
54198
55264
|
if (save.length > 0) {
|
|
54199
55265
|
const briefFile = flag("brief-file");
|
|
54200
|
-
const brief = briefFile.length > 0 ? await
|
|
55266
|
+
const brief = briefFile.length > 0 ? await readFile28(path42.resolve(briefFile), "utf8") : flag("brief");
|
|
54201
55267
|
if (brief.trim().length === 0) {
|
|
54202
55268
|
failValidation4("--brief-file (preferred) or --brief is required: the brief is the recipe.");
|
|
54203
55269
|
}
|
|
@@ -54318,7 +55384,7 @@ registerSchema({
|
|
|
54318
55384
|
prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
|
|
54319
55385
|
}
|
|
54320
55386
|
});
|
|
54321
|
-
var
|
|
55387
|
+
var updateCommand4 = defineCommand196({
|
|
54322
55388
|
meta: {
|
|
54323
55389
|
name: "update",
|
|
54324
55390
|
description: "Stage a scheduled action update. Examples: baker scheduled-actions update <id> --enabled false | baker scheduled-actions update <id> --mode publish"
|
|
@@ -54417,7 +55483,7 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
|
|
|
54417
55483
|
list: listCommand16,
|
|
54418
55484
|
get: getCommand4,
|
|
54419
55485
|
create: createCommand3,
|
|
54420
|
-
update:
|
|
55486
|
+
update: updateCommand4,
|
|
54421
55487
|
delete: deleteCommand3,
|
|
54422
55488
|
templates: templatesCommand,
|
|
54423
55489
|
trigger: triggerCommand
|
|
@@ -54668,7 +55734,7 @@ function parseImageRefs(spec) {
|
|
|
54668
55734
|
}
|
|
54669
55735
|
var defaultDeps = {
|
|
54670
55736
|
ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
|
|
54671
|
-
upload: (
|
|
55737
|
+
upload: (path44) => uploadLocalImage({ file: path44, contentType: detectImageContentType(path44), source: "uploaded" })
|
|
54672
55738
|
};
|
|
54673
55739
|
async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
54674
55740
|
const refs = parseImageRefs(spec);
|
|
@@ -54688,10 +55754,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
|
|
|
54688
55754
|
}
|
|
54689
55755
|
return { imageIds, added };
|
|
54690
55756
|
}
|
|
54691
|
-
function uploadFailure(
|
|
55757
|
+
function uploadFailure(path44) {
|
|
54692
55758
|
return (error) => {
|
|
54693
55759
|
if (error instanceof ApiError) throw error;
|
|
54694
|
-
throw new ApiError("VALIDATION_ERROR", `Could not read "${
|
|
55760
|
+
throw new ApiError("VALIDATION_ERROR", `Could not read "${path44}" as an image.`);
|
|
54695
55761
|
};
|
|
54696
55762
|
}
|
|
54697
55763
|
|
|
@@ -55689,7 +56755,7 @@ import { defineCommand as defineCommand211 } from "citty";
|
|
|
55689
56755
|
import { defineCommand as defineCommand208 } from "citty";
|
|
55690
56756
|
|
|
55691
56757
|
// src/commands/tag-manager/shared.ts
|
|
55692
|
-
import { readFileSync as
|
|
56758
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
55693
56759
|
function failValidation5(message) {
|
|
55694
56760
|
writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
|
|
55695
56761
|
process.exit(1);
|
|
@@ -55705,7 +56771,7 @@ function requireTarget6(args, entity) {
|
|
|
55705
56771
|
function loadJsonArg2(args, flag = "json") {
|
|
55706
56772
|
const inline = args[flag];
|
|
55707
56773
|
const file = args.file;
|
|
55708
|
-
const raw = typeof file === "string" && file.length > 0 ?
|
|
56774
|
+
const raw = typeof file === "string" && file.length > 0 ? readFileSync18(file, "utf8") : typeof inline === "string" && inline.length > 0 ? inline : void 0;
|
|
55709
56775
|
if (raw === void 0) {
|
|
55710
56776
|
failValidation5(`pass --${flag} with inline JSON or --file with a path to a JSON file`);
|
|
55711
56777
|
}
|
|
@@ -55753,10 +56819,10 @@ async function stageOp4(op) {
|
|
|
55753
56819
|
handleError5(err);
|
|
55754
56820
|
}
|
|
55755
56821
|
}
|
|
55756
|
-
async function draftAction3(
|
|
56822
|
+
async function draftAction3(path44, body, chat) {
|
|
55757
56823
|
const chatId = resolveChatId(chat);
|
|
55758
56824
|
try {
|
|
55759
|
-
const data = await apiPost(
|
|
56825
|
+
const data = await apiPost(path44, { chatId, ...body });
|
|
55760
56826
|
writeJsonEnvelope({ ok: true, data });
|
|
55761
56827
|
return data;
|
|
55762
56828
|
} catch (err) {
|
|
@@ -56891,9 +57957,9 @@ var groupCommand2 = defineCommand219({
|
|
|
56891
57957
|
});
|
|
56892
57958
|
|
|
56893
57959
|
// src/commands/videos/ingest.ts
|
|
56894
|
-
import { mkdtemp as mkdtemp2, rm as rm8, stat as
|
|
57960
|
+
import { mkdtemp as mkdtemp2, rm as rm8, stat as stat10 } from "fs/promises";
|
|
56895
57961
|
import { tmpdir as tmpdir3 } from "os";
|
|
56896
|
-
import
|
|
57962
|
+
import path43 from "path";
|
|
56897
57963
|
import { defineCommand as defineCommand220 } from "citty";
|
|
56898
57964
|
|
|
56899
57965
|
// src/lib/streamUpload.ts
|
|
@@ -57244,7 +58310,7 @@ function ingestUrl(args) {
|
|
|
57244
58310
|
}
|
|
57245
58311
|
async function downloadThenIngest(args, country) {
|
|
57246
58312
|
const vimeoCookie = captureVimeoCookie();
|
|
57247
|
-
const workDir = await mkdtemp2(
|
|
58313
|
+
const workDir = await mkdtemp2(path43.join(tmpdir3(), "videos-ingest-"));
|
|
57248
58314
|
try {
|
|
57249
58315
|
const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
|
|
57250
58316
|
if (isAudioOnly(probe.info)) {
|
|
@@ -57275,7 +58341,7 @@ async function downloadThenIngest(args, country) {
|
|
|
57275
58341
|
// we allow to fetch it cannot drift apart.
|
|
57276
58342
|
timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
|
|
57277
58343
|
});
|
|
57278
|
-
const stats = await
|
|
58344
|
+
const stats = await stat10(filePath);
|
|
57279
58345
|
if (stats.size > MAX_VIDEO_INGEST_BYTES) {
|
|
57280
58346
|
throw new ApiError(
|
|
57281
58347
|
"VALIDATION_ERROR",
|
|
@@ -57380,7 +58446,7 @@ var searchCommand4 = defineCommand221({
|
|
|
57380
58446
|
var tagsCommand6 = makeTagsCommand("videos", "video", "/api/videos/tags");
|
|
57381
58447
|
|
|
57382
58448
|
// src/commands/videos/upload.ts
|
|
57383
|
-
import { readFile as
|
|
58449
|
+
import { readFile as readFile29, stat as stat11 } from "fs/promises";
|
|
57384
58450
|
import { basename as basename3, extname as extname4 } from "path";
|
|
57385
58451
|
import { defineCommand as defineCommand222 } from "citty";
|
|
57386
58452
|
var MIME_MAP = {
|
|
@@ -57464,7 +58530,7 @@ var uploadCommand2 = defineCommand222({
|
|
|
57464
58530
|
const originalFilename = basename3(filePath);
|
|
57465
58531
|
const descriptionContext = args.context;
|
|
57466
58532
|
if (args["dry-run"]) {
|
|
57467
|
-
const fileStats = await
|
|
58533
|
+
const fileStats = await stat11(filePath);
|
|
57468
58534
|
writeJson({
|
|
57469
58535
|
ok: true,
|
|
57470
58536
|
dryRun: true,
|
|
@@ -57477,7 +58543,7 @@ var uploadCommand2 = defineCommand222({
|
|
|
57477
58543
|
originalFilename,
|
|
57478
58544
|
descriptionContext
|
|
57479
58545
|
});
|
|
57480
|
-
const fileBuffer = await
|
|
58546
|
+
const fileBuffer = await readFile29(filePath);
|
|
57481
58547
|
const uploadResponse = await fetch(uploadUrl, {
|
|
57482
58548
|
method: "PUT",
|
|
57483
58549
|
headers: { "Content-Type": contentType },
|
|
@@ -58864,7 +59930,7 @@ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
|
|
|
58864
59930
|
};
|
|
58865
59931
|
}
|
|
58866
59932
|
function commandPathOf(root, argv) {
|
|
58867
|
-
const
|
|
59933
|
+
const path44 = [];
|
|
58868
59934
|
let command = root;
|
|
58869
59935
|
for (const token of argv) {
|
|
58870
59936
|
if (token === "--" || token.startsWith("-")) {
|
|
@@ -58875,10 +59941,10 @@ function commandPathOf(root, argv) {
|
|
|
58875
59941
|
if (next === void 0 || typeof next !== "object") {
|
|
58876
59942
|
break;
|
|
58877
59943
|
}
|
|
58878
|
-
|
|
59944
|
+
path44.push(token);
|
|
58879
59945
|
command = next;
|
|
58880
59946
|
}
|
|
58881
|
-
return
|
|
59947
|
+
return path44.join(" ");
|
|
58882
59948
|
}
|
|
58883
59949
|
function refuseUnknownFlags(root, argv) {
|
|
58884
59950
|
const unknown = findUnknownFlags(root, argv);
|
|
@@ -58896,7 +59962,7 @@ function refuseUnknownFlags(root, argv) {
|
|
|
58896
59962
|
}
|
|
58897
59963
|
|
|
58898
59964
|
// src/version.ts
|
|
58899
|
-
import { readFileSync as
|
|
59965
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
58900
59966
|
function packageJsonUrl() {
|
|
58901
59967
|
return new URL("../package.json", import.meta.url);
|
|
58902
59968
|
}
|
|
@@ -58908,7 +59974,7 @@ function parsePackageVersion(raw) {
|
|
|
58908
59974
|
throw new Error("Invalid CLI package.json: missing version");
|
|
58909
59975
|
}
|
|
58910
59976
|
function getCliVersion() {
|
|
58911
|
-
return parsePackageVersion(
|
|
59977
|
+
return parsePackageVersion(readFileSync19(packageJsonUrl(), "utf8"));
|
|
58912
59978
|
}
|
|
58913
59979
|
|
|
58914
59980
|
// src/cli.ts
|
|
@@ -58947,7 +60013,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
|
|
|
58947
60013
|
tags: tagsCommand4,
|
|
58948
60014
|
"tag-manager": tagManagerCommand,
|
|
58949
60015
|
chats: chatsCommand,
|
|
58950
|
-
history:
|
|
60016
|
+
history: historyCommand2,
|
|
58951
60017
|
hubspot: hubspotCommand,
|
|
58952
60018
|
"winning-ads": winningAdsCommand,
|
|
58953
60019
|
mcp: mcpCommand,
|