@erdoai/cli 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +96 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1298,9 +1298,45 @@ wsCmd.command("attach <slug>").description("Link a resource (page, dataset, hear
1298
1298
  }
1299
1299
  }
1300
1300
  );
1301
+ var VARIANT_SPEC_KEYS = /* @__PURE__ */ new Set([
1302
+ "key",
1303
+ "label",
1304
+ "alloc",
1305
+ "allocation_percent",
1306
+ "control",
1307
+ "is_control",
1308
+ "page",
1309
+ "url",
1310
+ "treatment_url",
1311
+ "type",
1312
+ "treatment_resource_type",
1313
+ "resource",
1314
+ "treatment_resource_id",
1315
+ "class",
1316
+ "treatment_class"
1317
+ ]);
1318
+ var COMMA_SAFE_VARIANT_KEYS = /* @__PURE__ */ new Set(["url", "treatment_url", "label"]);
1319
+ function splitVariantSpec(spec) {
1320
+ const parts = [];
1321
+ let lastKey = "";
1322
+ for (const raw of spec.split(",")) {
1323
+ const trimmed = raw.trim();
1324
+ if (!trimmed) continue;
1325
+ const eq = trimmed.indexOf("=");
1326
+ const key = eq > 0 ? trimmed.slice(0, eq).trim() : "";
1327
+ const startsToken = trimmed === "control" || trimmed === "is_control" || key !== "" && VARIANT_SPEC_KEYS.has(key);
1328
+ if (!startsToken && parts.length > 0 && COMMA_SAFE_VARIANT_KEYS.has(lastKey)) {
1329
+ parts[parts.length - 1] += "," + raw;
1330
+ continue;
1331
+ }
1332
+ parts.push(raw);
1333
+ lastKey = startsToken && key !== "" ? key : "";
1334
+ }
1335
+ return parts;
1336
+ }
1301
1337
  function parseVariant(spec) {
1302
1338
  const v = {};
1303
- for (const raw of spec.split(",")) {
1339
+ for (const raw of splitVariantSpec(spec)) {
1304
1340
  const part = raw.trim();
1305
1341
  if (!part) continue;
1306
1342
  const eq = part.indexOf("=");
@@ -1309,7 +1345,7 @@ function parseVariant(spec) {
1309
1345
  v.is_control = true;
1310
1346
  continue;
1311
1347
  }
1312
- throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, alloc=, class=, or 'control')`);
1348
+ throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, url=, alloc=, class=, or 'control')`);
1313
1349
  }
1314
1350
  const k = part.slice(0, eq).trim();
1315
1351
  const val = part.slice(eq + 1).trim();
@@ -1333,6 +1369,10 @@ function parseVariant(spec) {
1333
1369
  v.treatment_resource_type = "page";
1334
1370
  v.treatment_resource_id = val;
1335
1371
  break;
1372
+ case "url":
1373
+ case "treatment_url":
1374
+ v.treatment_url = val;
1375
+ break;
1336
1376
  case "type":
1337
1377
  case "treatment_resource_type":
1338
1378
  v.treatment_resource_type = val;
@@ -1352,6 +1392,9 @@ function parseVariant(spec) {
1352
1392
  if (!v.key) {
1353
1393
  throw new Error('--variant requires key=, e.g. --variant "key=control,label=Current,control"');
1354
1394
  }
1395
+ if (v.treatment_url && (v.treatment_resource_type || v.treatment_resource_id)) {
1396
+ throw new Error("--variant: url= is mutually exclusive with page=/type=/resource= (a variant is either an Erdo page or an external URL)");
1397
+ }
1355
1398
  if (!v.label) v.label = v.key;
1356
1399
  return v;
1357
1400
  }
@@ -1412,7 +1455,7 @@ expCmd.command("policy <slug>").description(
1412
1455
  });
1413
1456
  expCmd.command("create").description("Create an experiment with its variants, evidence datasets and decision rule").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "experiment slug (unique per org)").requiredOption("--title <title>", "title").option("--workstream <slug>", "host workstream").option("--scope <scope>", "local (default), platform_canary, platform_global").option("--hypothesis <text>", "hypothesis markdown").option("--primary-metric <metric>", "the metric the decision rule reads").option("--decision-rule <text>", "the rule that decides ship/stop from the primary metric").option("--guardrail <metric>", "a metric that must not regress (repeatable)", collect, []).option(
1414
1457
  "--variant <spec>",
1415
- 'a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page=, alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild',
1458
+ `a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page= (Erdo artifact id) OR url=<https://\u2026> (an external page the persona panel visits, e.g. a client's live landing page \u2014 mutually exclusive with page=), alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild`,
1416
1459
  collect,
1417
1460
  []
1418
1461
  ).option(
@@ -1980,34 +2023,60 @@ attnCmd.command("list").description("List attention items").option("--status <st
1980
2023
  }
1981
2024
  );
1982
2025
  attnCmd.command("respond <id>").description(
1983
- `Respond to an attention item. A choice answer's shape is {"<question_id>": ["<option>", ...]}. Answering a choice mapped to experiment variants records your pick as a human comparison in the calibration ledger.`
1984
- ).option("--answer <json>", `a JSON answer, e.g. '{"q1":["option-a"]}'`).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").action(async (id, opts) => {
1985
- try {
1986
- const given = [opts.answer !== void 0, !!opts.ack, !!opts.dismiss].filter(Boolean).length;
1987
- if (given !== 1) {
1988
- fail(new Error("provide exactly one of --answer, --ack, or --dismiss"));
1989
- }
1990
- let input;
1991
- if (opts.answer !== void 0) {
1992
- let parsed;
1993
- try {
1994
- parsed = JSON.parse(opts.answer);
1995
- } catch {
1996
- throw new Error(
1997
- `--answer must be valid JSON, e.g. '{"<question_id>": ["<option>", ...]}'`
2026
+ `Respond to an attention item. A choice answer's shape is {"<question_id>": ["<option>", ...]}. Answering a choice mapped to experiment variants records your pick as a human comparison in the calibration ledger. Use --flag-broken to flag one or more variants as broken (recorded as defect observations judge generation reads) \u2014 on its own or alongside --answer.`
2027
+ ).option("--answer <json>", `a JSON answer, e.g. '{"q1":["option-a"]}'`).option(
2028
+ "--flag-broken <json>",
2029
+ 'flag choice variants as broken: JSON array [{"variant_key":"...","reason":"..."}]'
2030
+ ).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").action(
2031
+ async (id, opts) => {
2032
+ try {
2033
+ if (opts.ack || opts.dismiss) {
2034
+ if (opts.ack && opts.dismiss) {
2035
+ fail(new Error("provide only one of --ack or --dismiss"));
2036
+ }
2037
+ if (opts.answer !== void 0 || opts.flagBroken !== void 0) {
2038
+ fail(new Error("--answer / --flag-broken cannot be combined with --ack or --dismiss"));
2039
+ }
2040
+ print(
2041
+ await new ErdoClient().respondAttentionItem(id, {
2042
+ action: opts.ack ? "acknowledge" : "dismiss"
2043
+ })
1998
2044
  );
2045
+ return;
1999
2046
  }
2000
- input = { action: "answer", answer: parsed };
2001
- } else if (opts.ack) {
2002
- input = { action: "acknowledge" };
2003
- } else {
2004
- input = { action: "dismiss" };
2047
+ if (opts.answer === void 0 && opts.flagBroken === void 0) {
2048
+ fail(new Error("provide one of --answer, --flag-broken, --ack, or --dismiss"));
2049
+ }
2050
+ const input = { action: "answer" };
2051
+ if (opts.answer !== void 0) {
2052
+ try {
2053
+ input.answer = JSON.parse(opts.answer);
2054
+ } catch {
2055
+ throw new Error(
2056
+ `--answer must be valid JSON, e.g. '{"<question_id>": ["<option>", ...]}'`
2057
+ );
2058
+ }
2059
+ }
2060
+ if (opts.flagBroken !== void 0) {
2061
+ let parsed;
2062
+ try {
2063
+ parsed = JSON.parse(opts.flagBroken);
2064
+ } catch {
2065
+ throw new Error(
2066
+ `--flag-broken must be valid JSON, e.g. '[{"variant_key":"b","reason":"clipped hero"}]'`
2067
+ );
2068
+ }
2069
+ if (!Array.isArray(parsed)) {
2070
+ throw new Error('--flag-broken must be a JSON array of {"variant_key","reason"} objects');
2071
+ }
2072
+ input.defects = parsed;
2073
+ }
2074
+ print(await new ErdoClient().respondAttentionItem(id, input));
2075
+ } catch (e) {
2076
+ fail(e);
2005
2077
  }
2006
- print(await new ErdoClient().respondAttentionItem(id, input));
2007
- } catch (e) {
2008
- fail(e);
2009
2078
  }
2010
- });
2079
+ );
2011
2080
  var reviewsCmd = program.command("reviews").description(
2012
2081
  "The Knowledge Review queue \u2014 knowledge patches the critic proposes, investigations, and deduplicated failure signals awaiting a human decision"
2013
2082
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Erdo CLI \u2014 drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {