@thinkingai/ae-cli 6.0.39 → 6.0.40

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.
@@ -209,6 +209,362 @@ var flowModifyBaseInfo = createEngageFlowCapabilityCommand({
209
209
  })
210
210
  });
211
211
 
212
+ // src/commands/te-engage/semantic-qp-validation.ts
213
+ var OPERATORS = /* @__PURE__ */ new Set([
214
+ "eq",
215
+ "neq",
216
+ "lt",
217
+ "lte",
218
+ "gt",
219
+ "gte",
220
+ "exists",
221
+ "not_exists",
222
+ "between",
223
+ "contains",
224
+ "not_contains",
225
+ "is_true",
226
+ "is_false",
227
+ "regex",
228
+ "not_regex",
229
+ "relative_current_time",
230
+ "relative_event_time",
231
+ "array_contains",
232
+ "in_cluster",
233
+ "not_in_cluster"
234
+ ]);
235
+ var AUDIENCE_AGGREGATIONS = /* @__PURE__ */ new Set([
236
+ "count",
237
+ "active_days",
238
+ "sum",
239
+ "avg",
240
+ "max",
241
+ "min",
242
+ "distinct_count"
243
+ ]);
244
+ var METRIC_AGGREGATIONS = /* @__PURE__ */ new Set([
245
+ "total_count",
246
+ "user_count",
247
+ "per_user_count",
248
+ "sum",
249
+ "avg",
250
+ "avg_per_user",
251
+ "max",
252
+ "min",
253
+ "distinct_count",
254
+ "median",
255
+ "percentile",
256
+ "variance",
257
+ "stddev"
258
+ ]);
259
+ function validateSemanticAudienceDefinition(value, path) {
260
+ const definition = object(value, path);
261
+ rejectUnknown(definition, ["type", "conditions", "include_filter", "exclude_filter"], path);
262
+ exactString(definition.type, "condition", `${path}.type`);
263
+ validateGroup(definition.conditions, `${path}.conditions`);
264
+ optional(definition.include_filter, (item) => validateGroup(item, `${path}.include_filter`));
265
+ optional(definition.exclude_filter, (item) => validateGroup(item, `${path}.exclude_filter`));
266
+ }
267
+ function validateSemanticEventDefinition(value, path) {
268
+ const event = object(value, path);
269
+ rejectUnknown(event, [
270
+ "type",
271
+ "event",
272
+ "operator",
273
+ "value",
274
+ "aggregation",
275
+ "property",
276
+ "time_range",
277
+ "filters"
278
+ ], path);
279
+ exactString(event.type, "event", `${path}.type`);
280
+ nonBlankString(event.event, `${path}.event`);
281
+ optionalEnum(event.operator, OPERATORS, `${path}.operator`);
282
+ optionalNumber(event.value, `${path}.value`);
283
+ optionalEnum(event.aggregation, AUDIENCE_AGGREGATIONS, `${path}.aggregation`);
284
+ optionalString(event.property, `${path}.property`);
285
+ optional(event.time_range, (item) => validateTimeRange(item, `${path}.time_range`));
286
+ optional(event.filters, (item) => validateFilterGroup(item, `${path}.filters`));
287
+ }
288
+ function validateSemanticMetricDefinition(value, path) {
289
+ const metric = object(value, path);
290
+ if (metric.type === "event") {
291
+ validateMetricEvent(metric, path, false);
292
+ return;
293
+ }
294
+ if (metric.type === "formula") {
295
+ rejectUnknown(metric, ["type", "expression", "dependencies", "format"], path);
296
+ nonBlankString(metric.expression, `${path}.expression`);
297
+ optionalString(metric.format, `${path}.format`);
298
+ array(metric.dependencies, `${path}.dependencies`, 1).forEach((dependency, index) => validateMetricEvent(
299
+ object(dependency, `${path}.dependencies[${index}]`),
300
+ `${path}.dependencies[${index}]`,
301
+ true
302
+ ));
303
+ return;
304
+ }
305
+ invalid(`${path}.type must be event or formula.`);
306
+ }
307
+ function validateEmbeddedSemanticDefinitions(value, path) {
308
+ walk(value, path);
309
+ }
310
+ function walk(value, path) {
311
+ if (Array.isArray(value)) {
312
+ value.forEach((item, index) => walk(item, `${path}[${index}]`));
313
+ return;
314
+ }
315
+ if (!isObject(value)) return;
316
+ for (const [key, item] of Object.entries(value)) {
317
+ const itemPath = `${path}.${key}`;
318
+ if (item === void 0 || item === null) continue;
319
+ if (["definitionRequest", "targetDefinitionRequest", "topicDefinitionRequest"].includes(key)) {
320
+ validateSemanticAudienceDefinition(item, itemPath);
321
+ } else if (key === "eventDefinition") {
322
+ validateSemanticEventDefinition(item, itemPath);
323
+ } else if (key === "triggerDefinition") {
324
+ validateTriggerDefinition(item, itemPath);
325
+ } else if (key === "config" && typeof item === "string") {
326
+ walkJsonString(item, itemPath);
327
+ } else {
328
+ walk(item, itemPath);
329
+ }
330
+ }
331
+ }
332
+ function validateTriggerDefinition(value, path) {
333
+ const definition = object(value, path);
334
+ rejectUnknown(definition, ["rules"], path);
335
+ array(definition.rules, `${path}.rules`, 1).forEach((ruleValue, ruleIndex) => {
336
+ const rulePath = `${path}.rules[${ruleIndex}]`;
337
+ const rule = object(ruleValue, rulePath);
338
+ array(rule.events, `${rulePath}.events`, 1).forEach((event, eventIndex) => {
339
+ const eventPath = `${rulePath}.events[${eventIndex}]`;
340
+ const candidate = object(event, eventPath);
341
+ if ("eventDefinition" in candidate) {
342
+ validateSemanticEventDefinition(candidate.eventDefinition, `${eventPath}.eventDefinition`);
343
+ } else {
344
+ validateSemanticEventDefinition(candidate, eventPath);
345
+ }
346
+ });
347
+ optional(rule.blackList, (items) => array(items, `${rulePath}.blackList`).forEach((event, index) => validateSemanticEventDefinition(
348
+ event,
349
+ `${rulePath}.blackList[${index}]`
350
+ )));
351
+ });
352
+ }
353
+ function validateGroup(value, path) {
354
+ const group = object(value, path);
355
+ rejectUnknown(group, ["relation", "items"], path);
356
+ enumValue(group.relation, /* @__PURE__ */ new Set(["and", "or"]), `${path}.relation`);
357
+ array(group.items, `${path}.items`, 1).forEach((item, index) => validateCondition(item, `${path}.items[${index}]`));
358
+ }
359
+ function validateCondition(value, path) {
360
+ const condition = object(value, path);
361
+ switch (condition.type) {
362
+ case "event":
363
+ validateSemanticEventDefinition(condition, path);
364
+ required(condition.time_range, `${path}.time_range`);
365
+ return;
366
+ case "user":
367
+ case "tag":
368
+ case "cluster":
369
+ validatePropertyCondition(condition, path);
370
+ return;
371
+ case "compound":
372
+ rejectUnknown(condition, ["type", "group"], path);
373
+ validateGroup(condition.group, `${path}.group`);
374
+ return;
375
+ case "behavior_sequence":
376
+ validateBehaviorSequence(condition, path);
377
+ return;
378
+ default:
379
+ invalid(`${path}.type is unsupported.`);
380
+ }
381
+ }
382
+ function validatePropertyCondition(condition, path) {
383
+ const extended = condition.type === "tag" || condition.type === "cluster";
384
+ rejectUnknown(condition, [
385
+ "type",
386
+ "field",
387
+ "operator",
388
+ "values",
389
+ "time_relative",
390
+ "time_unit",
391
+ ...extended ? ["cluster_date_policy", "specified_cluster_date"] : []
392
+ ], path);
393
+ validateField(condition.field, `${path}.field`);
394
+ if (condition.type === "user") enumValue(condition.operator, OPERATORS, `${path}.operator`);
395
+ else optionalEnum(condition.operator, OPERATORS, `${path}.operator`);
396
+ optional(condition.values, (item) => array(item, `${path}.values`));
397
+ optionalString(condition.time_relative, `${path}.time_relative`);
398
+ optionalString(condition.time_unit, `${path}.time_unit`);
399
+ optionalString(condition.cluster_date_policy, `${path}.cluster_date_policy`);
400
+ optionalString(condition.specified_cluster_date, `${path}.specified_cluster_date`);
401
+ }
402
+ function validateBehaviorSequence(condition, path) {
403
+ rejectUnknown(condition, ["type", "completed", "steps", "time_range", "window"], path);
404
+ if (typeof condition.completed !== "boolean") invalid(`${path}.completed must be a boolean.`);
405
+ array(condition.steps, `${path}.steps`, 1).forEach((stepValue, index) => {
406
+ const stepPath = `${path}.steps[${index}]`;
407
+ const step = object(stepValue, stepPath);
408
+ rejectUnknown(step, ["event", "completed", "filters", "relative_to_first", "window"], stepPath);
409
+ nonBlankString(step.event, `${stepPath}.event`);
410
+ optionalBoolean(step.completed, `${stepPath}.completed`);
411
+ optionalBoolean(step.relative_to_first, `${stepPath}.relative_to_first`);
412
+ if (index === 1 && step.relative_to_first === true) {
413
+ invalid(`${stepPath}.relative_to_first must be false or omitted for the second step.`);
414
+ }
415
+ optional(step.filters, (item) => validateFilterGroup(item, `${stepPath}.filters`));
416
+ optional(step.window, (item) => validateWindow(item, `${stepPath}.window`));
417
+ });
418
+ required(condition.time_range, `${path}.time_range`);
419
+ validateTimeRange(condition.time_range, `${path}.time_range`);
420
+ optional(condition.window, (item) => validateWindow(item, `${path}.window`));
421
+ }
422
+ function validateFilterGroup(value, path) {
423
+ const group = object(value, path);
424
+ rejectUnknown(group, ["relation", "items"], path);
425
+ optionalEnum(group.relation, /* @__PURE__ */ new Set(["and", "or"]), `${path}.relation`);
426
+ array(group.items, `${path}.items`, 1).forEach((filterValue, index) => {
427
+ const filterPath = `${path}.items[${index}]`;
428
+ const filter = object(filterValue, filterPath);
429
+ rejectUnknown(filter, ["field", "operator", "values"], filterPath);
430
+ validateField(filter.field, `${filterPath}.field`);
431
+ enumValue(filter.operator, OPERATORS, `${filterPath}.operator`);
432
+ optional(filter.values, (item) => array(item, `${filterPath}.values`));
433
+ });
434
+ }
435
+ function validateField(value, path) {
436
+ if (typeof value === "string") {
437
+ nonBlankString(value, path);
438
+ return;
439
+ }
440
+ const field = object(value, path);
441
+ rejectUnknown(field, ["name", "type"], path);
442
+ nonBlankString(field.name, `${path}.name`);
443
+ optionalEnum(field.type, /* @__PURE__ */ new Set([
444
+ "event_property",
445
+ "user_property",
446
+ "tag",
447
+ "cluster"
448
+ ]), `${path}.type`);
449
+ }
450
+ function validateTimeRange(value, path) {
451
+ const range = object(value, path);
452
+ rejectUnknown(range, ["mode", "unit", "value", "start_time", "end_time"], path);
453
+ if (range.mode === "custom") {
454
+ nonBlankString(range.start_time, `${path}.start_time`);
455
+ nonBlankString(range.end_time, `${path}.end_time`);
456
+ return;
457
+ }
458
+ enumValue(range.mode, /* @__PURE__ */ new Set(["recent", "previous"]), `${path}.mode`);
459
+ optional(range.unit, (unit) => exactString(unit, "day", `${path}.unit`));
460
+ positiveInteger(range.value, `${path}.value`);
461
+ }
462
+ function validateWindow(value, path) {
463
+ const window = object(value, path);
464
+ rejectUnknown(window, ["value", "unit"], path);
465
+ positiveInteger(window.value, `${path}.value`);
466
+ nonBlankString(window.unit, `${path}.unit`);
467
+ }
468
+ function validateMetricEvent(metric, path, dependency) {
469
+ rejectUnknown(metric, [
470
+ "type",
471
+ ...dependency ? ["key"] : [],
472
+ "event",
473
+ "aggregation",
474
+ "property",
475
+ "percentile",
476
+ "filters",
477
+ "display_name"
478
+ ], path);
479
+ exactString(metric.type, "event", `${path}.type`);
480
+ if (dependency) nonBlankString(metric.key, `${path}.key`);
481
+ nonBlankString(metric.event, `${path}.event`);
482
+ enumValue(metric.aggregation, METRIC_AGGREGATIONS, `${path}.aggregation`);
483
+ if (!["total_count", "user_count", "per_user_count"].includes(String(metric.aggregation))) {
484
+ nonBlankString(metric.property, `${path}.property`);
485
+ }
486
+ optionalString(metric.property, `${path}.property`);
487
+ if (metric.aggregation === "percentile") {
488
+ optionalNumber(metric.percentile, `${path}.percentile`);
489
+ if (typeof metric.percentile !== "number" || metric.percentile <= 0 || metric.percentile > 100) {
490
+ invalid(`${path}.percentile must be greater than 0 and at most 100.`);
491
+ }
492
+ } else if (metric.percentile !== void 0 && metric.percentile !== null) {
493
+ invalid(`${path}.percentile is only valid for percentile aggregation.`);
494
+ }
495
+ optionalString(metric.display_name, `${path}.display_name`);
496
+ optional(metric.filters, (item) => validateFilterGroup(item, `${path}.filters`));
497
+ }
498
+ function walkJsonString(value, path) {
499
+ try {
500
+ walk(JSON.parse(value), path);
501
+ } catch (error) {
502
+ if (error instanceof SyntaxError) invalid(`${path} must contain valid JSON.`);
503
+ throw error;
504
+ }
505
+ }
506
+ function object(value, path) {
507
+ if (!isObject(value)) invalid(`${path} must be a JSON object.`);
508
+ return value;
509
+ }
510
+ function isObject(value) {
511
+ return value !== null && typeof value === "object" && !Array.isArray(value);
512
+ }
513
+ function array(value, path, minLength = 0) {
514
+ if (!Array.isArray(value) || value.length < minLength) {
515
+ invalid(`${path} must be an array with at least ${minLength} item(s).`);
516
+ }
517
+ return value;
518
+ }
519
+ function rejectUnknown(value, allowed, path) {
520
+ const supported = new Set(allowed);
521
+ const unknown = Object.keys(value).find((key) => !supported.has(key));
522
+ if (unknown) invalid(`Unsupported field ${path}.${unknown}.`);
523
+ }
524
+ function optional(value, validate) {
525
+ if (value !== void 0 && value !== null) validate(value);
526
+ }
527
+ function required(value, path) {
528
+ if (value === void 0 || value === null) invalid(`${path} is required.`);
529
+ }
530
+ function nonBlankString(value, path) {
531
+ if (typeof value !== "string" || value.trim() === "") {
532
+ invalid(`${path} must be a non-blank string.`);
533
+ }
534
+ }
535
+ function optionalString(value, path) {
536
+ optional(value, (item) => nonBlankString(item, path));
537
+ }
538
+ function exactString(value, expected, path) {
539
+ if (value !== expected) invalid(`${path} must be ${expected}.`);
540
+ }
541
+ function enumValue(value, allowed, path) {
542
+ if (typeof value !== "string" || !allowed.has(value)) {
543
+ invalid(`${path} must be one of: ${[...allowed].join(", ")}.`);
544
+ }
545
+ }
546
+ function optionalEnum(value, allowed, path) {
547
+ optional(value, (item) => enumValue(item, allowed, path));
548
+ }
549
+ function optionalNumber(value, path) {
550
+ optional(value, (item) => {
551
+ if (typeof item !== "number" || !Number.isFinite(item)) invalid(`${path} must be a number.`);
552
+ });
553
+ }
554
+ function positiveInteger(value, path) {
555
+ if (!Number.isInteger(value) || value < 1) {
556
+ invalid(`${path} must be a positive integer.`);
557
+ }
558
+ }
559
+ function optionalBoolean(value, path) {
560
+ optional(value, (item) => {
561
+ if (typeof item !== "boolean") invalid(`${path} must be a boolean.`);
562
+ });
563
+ }
564
+ function invalid(message) {
565
+ throw new CliValidationError(message);
566
+ }
567
+
212
568
  // src/commands/te-engage/engage-flow/flow/save.ts
213
569
  var allowedOperations = ["build", "preview", "commit"];
214
570
  function readRequest(ctx) {
@@ -223,6 +579,7 @@ function readRequest(ctx) {
223
579
  if ("sourceFlowUuid" in req) {
224
580
  throw new Error("Flag --req.sourceFlowUuid is unsupported; get the source flow and build nodes/edges instead");
225
581
  }
582
+ validateEmbeddedSemanticDefinitions(req, "--req");
226
583
  return req;
227
584
  }
228
585
  var flowSave = createEngageFlowCapabilityCommand({
@@ -1042,6 +1399,12 @@ var presetEventUpdate = createEngageSettingCapabilityCommand({
1042
1399
  { name: "recharge-event-definition", type: "json", required: false, desc: "Semantic recharge event definition." }
1043
1400
  ],
1044
1401
  risk: "write",
1402
+ validate: (ctx) => {
1403
+ for (const name of ["add-event-definition", "active-event-definition", "recharge-event-definition"]) {
1404
+ const definition = ctx.json(name);
1405
+ if (definition !== void 0) validateSemanticEventDefinition(definition, `--${name}`);
1406
+ }
1407
+ },
1045
1408
  buildInput: (ctx) => ({
1046
1409
  project_id: ctx.num("project-id"),
1047
1410
  add_event_definition: ctx.json("add-event-definition") || void 0,
@@ -1085,23 +1448,7 @@ var commonMetricGet = createEngageSettingCapabilityCommand({
1085
1448
  // src/commands/te-engage/engage-setting/common-metric/metric-qp-validation.ts
1086
1449
  var VALID_WINDOW_TIME_UNITS = /* @__PURE__ */ new Set(["minute", "hour", "day"]);
1087
1450
  function validateMetricDefinitionFlag(definition) {
1088
- if (definition === null || typeof definition !== "object" || Array.isArray(definition)) {
1089
- printError(
1090
- "validation",
1091
- "--metric-definition must be a JSON object.",
1092
- "Pass a semantic event or formula metric definition."
1093
- );
1094
- process.exit(1);
1095
- }
1096
- const type = definition.type;
1097
- if (type !== "event" && type !== "formula") {
1098
- printError(
1099
- "validation",
1100
- "--metric-definition.type must be event or formula.",
1101
- "Do not pass legacy type=0/1 metric_qp."
1102
- );
1103
- process.exit(1);
1104
- }
1451
+ validateSemanticMetricDefinition(definition, "--metric-definition");
1105
1452
  }
1106
1453
  function validateMetricWindowTimeUnitFlag(unit) {
1107
1454
  if (VALID_WINDOW_TIME_UNITS.has(unit)) {
@@ -1587,6 +1934,16 @@ var presetMetricSet = createEngageSceneCapabilityCommand({
1587
1934
  { name: "attend-event-definition", type: "json", required: false, desc: "Semantic attend event definition." }
1588
1935
  ],
1589
1936
  risk: "write",
1937
+ validate: (ctx) => {
1938
+ for (const name of [
1939
+ "impression-event-definition",
1940
+ "click-event-definition",
1941
+ "attend-event-definition"
1942
+ ]) {
1943
+ const definition = ctx.json(name);
1944
+ if (definition !== void 0) validateSemanticEventDefinition(definition, `--${name}`);
1945
+ }
1946
+ },
1590
1947
  buildInput: (ctx) => ({
1591
1948
  project_id: ctx.num("project-id"),
1592
1949
  config_id: ctx.str("config-id"),
@@ -1865,9 +2222,13 @@ var strategyCreate = createEngageSceneCapabilityCommand({
1865
2222
  }
1866
2223
  ],
1867
2224
  risk: "write",
2225
+ validate: (ctx) => {
2226
+ const payload = readRequiredJsonObject(ctx, "payload");
2227
+ validateEmbeddedSemanticDefinitions(payload, "--payload");
2228
+ },
1868
2229
  buildInput: (ctx) => ({
1869
2230
  project_id: ctx.num("project-id"),
1870
- payload: ctx.json("payload")
2231
+ payload: readRequiredJsonObject(ctx, "payload")
1871
2232
  })
1872
2233
  });
1873
2234
 
@@ -1964,9 +2325,13 @@ var strategyUpdate = createEngageSceneCapabilityCommand({
1964
2325
  }
1965
2326
  ],
1966
2327
  risk: "write",
2328
+ validate: (ctx) => {
2329
+ const payload = readRequiredJsonObject(ctx, "payload");
2330
+ validateEmbeddedSemanticDefinitions(payload, "--payload");
2331
+ },
1967
2332
  buildInput: (ctx) => ({
1968
2333
  project_id: ctx.num("project-id"),
1969
- payload: ctx.json("payload")
2334
+ payload: readRequiredJsonObject(ctx, "payload")
1970
2335
  })
1971
2336
  });
1972
2337
 
@@ -2022,7 +2387,10 @@ var strategyPredict = createEngageSceneCapabilityCommand({
2022
2387
  ],
2023
2388
  risk: "read",
2024
2389
  validate: (ctx) => {
2025
- readRequiredJsonObject(ctx, "definition-request");
2390
+ validateSemanticAudienceDefinition(
2391
+ readRequiredJsonObject(ctx, "definition-request"),
2392
+ "--definition-request"
2393
+ );
2026
2394
  },
2027
2395
  buildInput: (ctx) => ({
2028
2396
  project_id: ctx.num("project-id"),
@@ -2513,6 +2881,7 @@ var TOPIC_TASK_EXCLUSION_FIELDS = /* @__PURE__ */ new Set([
2513
2881
  ]);
2514
2882
  function validateStandaloneActivityPayload(value) {
2515
2883
  const payload = requireObject(value);
2884
+ validateEmbeddedSemanticDefinitions(payload, "payload");
2516
2885
  validateTriggerType(payload.triggerType, "triggerType");
2517
2886
  validateScheduleFields(payload);
2518
2887
  validateExperiment(payload.expConfig, "expConfig");
@@ -2536,6 +2905,7 @@ function validateStandaloneActivityPayload(value) {
2536
2905
  }
2537
2906
  function validateActivityTopicPayload(value) {
2538
2907
  const payload = requireObject(value);
2908
+ validateEmbeddedSemanticDefinitions(payload, "payload");
2539
2909
  validateTriggerType(payload.triggerType, "triggerType");
2540
2910
  validateScheduleFields(payload);
2541
2911
  validateExperiment(payload.expConfig, "expConfig");
@@ -2610,7 +2980,7 @@ function validateScheduleFields(payload) {
2610
2980
  }
2611
2981
  function validateExperiment(value, field) {
2612
2982
  if (value === void 0 || value === null) return;
2613
- if (!isObject(value)) {
2983
+ if (!isObject2(value)) {
2614
2984
  fail(
2615
2985
  `${field} must be omitted or set to {"enableExp":false}.`,
2616
2986
  "ACTIVITY_EXPERIMENT_UNSUPPORTED",
@@ -2641,7 +3011,7 @@ function validateSingleContentGroup(value, field) {
2641
3011
  }
2642
3012
  }
2643
3013
  function requireObject(value, field = "payload") {
2644
- if (!isObject(value)) {
3014
+ if (!isObject2(value)) {
2645
3015
  fail(
2646
3016
  `${field} must be a JSON object.`,
2647
3017
  "ACTIVITY_PAYLOAD_INVALID",
@@ -2653,7 +3023,7 @@ function requireObject(value, field = "payload") {
2653
3023
  }
2654
3024
  function containsAnyKey(value, keys) {
2655
3025
  if (Array.isArray(value)) return value.some((item) => containsAnyKey(item, keys));
2656
- if (!isObject(value)) return false;
3026
+ if (!isObject2(value)) return false;
2657
3027
  return Object.entries(value).some(
2658
3028
  ([key, item]) => keys.has(key) && hasValue(item) || containsAnyKey(item, keys)
2659
3029
  );
@@ -2662,10 +3032,10 @@ function hasValue(value) {
2662
3032
  if (value === void 0 || value === null) return false;
2663
3033
  if (typeof value === "string") return value.trim().length > 0;
2664
3034
  if (Array.isArray(value)) return value.length > 0;
2665
- if (isObject(value)) return Object.keys(value).length > 0;
3035
+ if (isObject2(value)) return Object.keys(value).length > 0;
2666
3036
  return true;
2667
3037
  }
2668
- function isObject(value) {
3038
+ function isObject2(value) {
2669
3039
  return typeof value === "object" && value !== null && !Array.isArray(value);
2670
3040
  }
2671
3041
  function fail(message, code, field, hint) {
@@ -3522,7 +3892,7 @@ var taskSave = createEngageTaskCapabilityCommand({
3522
3892
  ],
3523
3893
  risk: "write",
3524
3894
  validate: (ctx) => {
3525
- readRequiredJsonObject(ctx, "req");
3895
+ validateEmbeddedSemanticDefinitions(readRequiredJsonObject(ctx, "req"), "--req");
3526
3896
  },
3527
3897
  buildInput: (ctx) => ({ project_id: ctx.num("project-id"), req: readRequiredJsonObject(ctx, "req") })
3528
3898
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingai/ae-cli",
3
- "version": "6.0.39",
3
+ "version": "6.0.40",
4
4
  "description": "CLI tool for ThinkingAI (AE) analytics platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "verify:metadata-capability": "npx tsx test/metadata-capability-commands.test.mjs",
35
35
  "verify:community-capability": "node test/community-capability-routing.test.mjs",
36
36
  "verify:community-report": "npx tsx tests/community-report-standard-v5.test.ts && npx tsx tests/community-report-client.test.ts && npx tsx tests/community-report-framework.test.ts && npx tsx tests/community-data-report-command.test.ts && node test/community-data-report-skill.test.mjs",
37
- "verify:engage-capability": "tsx test/engage/engage-capability-command.test.mjs && node test/engage/engage-skill-capability.test.mjs",
37
+ "verify:engage-capability": "tsx test/engage/engage-capability-command.test.mjs && tsx test/engage/semantic-qp-validation.test.mjs && node test/engage/engage-skill-capability.test.mjs",
38
38
  "verify:retired-analysis-commands": "node --test test/retired-analysis-commands.test.mjs",
39
39
  "verify:analysis-meta-tools": "node scripts/verify-te-meta-tools.mjs",
40
40
  "verify:analysis-common-tools": "node scripts/verify-te-common-tools.mjs",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ae-engage
3
3
  version: 1.0.0
4
- description: "AE Engage capability gateway: config center, flows, push/config channels, strategies, templates, and task management. Trigger words: config center, scene config, push channel, config channel, operation strategy, operation task, template, config item, Engage, Hermes, engage-scene, engage-setting, engage-flow, engage-task."
4
+ description: "AE Engage capability gateway: config center, flows, push/config channels, strategies, templates, task management, and operation activities. Trigger words: config center, scene config, push channel, config channel, operation strategy, operation task, operation activity, template, config item, Engage, Hermes, engage-scene, engage-setting, engage-flow, engage-task, engage-activity."
5
5
  ---
6
6
 
7
7
  # ae-engage
@@ -56,6 +56,7 @@ When the user mentions a product term below (including common Chinese UI labels)
56
56
  | **Config channel** | Config-center Webhook/client config channels (not the same as push channels) | `engage-scene` | `references/scene-config-channel.md` | `channel-mgmt.md` (create/enable-disable/copy/delete workflows). User params in `config.customsParamList` require `columnName` with `user:` prefix (e.g. `user:#account_id`); preflight names with ae-analysis `analysis-meta property list/get`. |
57
57
  | **Operation strategy** | Ops/delivery strategies under a config item | `engage-scene` | `references/scene-strategy.md` | Custom audience: [`scene-strategy-audience.md`](references/scene-strategy-audience.md) — semantic `definitionRequest` (Analysis condition shape); do not pass `targetClusterQp`/`qp`; preflight props (stop + list if missing); template: `scene-template.md` |
58
58
  | **Operation task** | Hermes push/engagement tasks (list, save, lifecycle, reports) | `engage-task` | `references/task-list.md` | `task-detail.md` (get), `save-task.md`, `build-task-save-guide.md`, `task-stats.md`, `task-delete.md`, `push-record-query.md`, `task-data-overview.md`, `task-data-detail.md`, `task-metric-detail.md`, `task-experiment-report.md` |
59
+ | **Operation activity** | Campaign activity management and delivery trends by activity, topic, or standalone task | `engage-activity` | `references/activity-activity.md` | `activity-data-detail.md`, `activity-topic.md`, `activity-task.md`, `activity-approval.md` |
59
60
  | **Template** | Strategy templates under a config item | `engage-scene` | `references/scene-template.md` | `scene-config-param.md` (template fields reference `paramId`); enable via `template update` then `template update-status` before strategy create |
60
61
 
61
62
  **Easy to confuse:**
@@ -75,6 +76,7 @@ Naming boundary:
75
76
 
76
77
  - CLI flags use kebab-case; outer Capability input and all Capability response keys use snake_case.
77
78
  - Nested business DTOs passed through `--req` or `--payload` keep their documented native camelCase fields. Do not mechanically convert those nested DTO keys to snake_case.
79
+ - Semantic audience, event, trigger, completion, and metric definitions are closed contracts. The CLI rejects malformed or unknown semantic fields locally; `--validate` applies the same precise Hermes capability schema without writing.
78
80
  - Successful migrated commands return their business payload under `data`; read the matching reference's Response shape before selecting fields.
79
81
 
80
82
  ## JSON Parameter Format
@@ -375,6 +377,9 @@ ae-cli engage-setting channel list --project-id <projectId>
375
377
  5. `engage-flow flow save` is **operation-based** (protocol v2). The `--req` object must carry an `operation` of `build`, `preview`, or `commit`. Do **not** use the old `nodeList` / `edgeList` field names — use `nodes` / `edges` with `operation=build`. A legacy `nodeList`/`edgeList` payload (or a missing `operation`) is rejected with `Unsupported save_flow operation: null`.
376
378
  6. Run the lifecycle: `build` (returns `data.result.status = ready_to_preview` or `need_input`) → resolve any `data.result.next_slot` → `preview` (re-issues response fields `data.result.draft_version` + `data.result.confirm_token`) → `commit` (maps those values to request fields `draftVersion` + `confirmToken`) → reads the final ID from `data.result.result.flow_uuid`.
377
379
  7. `nodes[].config` / `edges[].config` may be a JSON object or a JSON string. Custom audience nodes and branches use semantic `targetDefinitionRequest`; Hermes compiles it to the node's stored execution format.
380
+ Never send `targetClusterQp`. Each audience `event` and `behavior_sequence` must include
381
+ its own `time_range`; Flow entry dates do not replace that range. Use only properties that
382
+ resolve through the Flow editor's current project, timezone, and user-entity metadata scope.
378
383
  8. You must self-check before previewing/committing:
379
384
  - There is exactly one entry node
380
385
  - There is at least one `exit_flow`
@@ -456,6 +461,7 @@ More detailed single-command guidance is available in the business-oriented `ref
456
461
  - `references/config-item-analysis-report.md` (`engage-scene.report.config-item-analysis`, L3)
457
462
  - `references/config-item-strategy-comparison.md` (`engage-scene.report.strategy-comparison`, L3)
458
463
  - `references/activity-activity.md` (`engage-activity.activity.{create,update,delete,list,get,pause,end,stats,info-list}`)
464
+ - `references/activity-data-detail.md` (`engage-activity.activity-data.detail`, L3)
459
465
  - `references/activity-approval.md` (`engage-activity.approval.{submit,approve,reject,cancel}`)
460
466
  - `references/activity-topic.md` (`engage-activity.topic.{create,update,remove-task,delete,get,copy}`)
461
467
  - `references/activity-activity-type.md` (`engage-activity.activity-type.{list,batch-add,update,batch-delete}`)
@@ -533,7 +539,9 @@ For task draft creation or update, use this workflow:
533
539
  `channelType`, `triggerType`, and `eventTriggerType` to `build-save-guide`, then use its
534
540
  type-specific semantic event shape. Accumulated events are aggregate conditions, continuous
535
541
  events use count/eq with a value of at least 2, ordered events use sequence-step envelopes,
536
- and every-completion events use count/eq/1. Never construct persisted QP fields.
542
+ and every-completion events use count/eq/1. Completion target and experiment main-goal event
543
+ filters must not use properties whose metadata `select_type` is `datetime`. Never construct
544
+ persisted QP fields.
537
545
  4. `ae-cli engage-task task save --project-id <projectId> --req '{...}'`
538
546
  5. `ae-cli engage-task task submit-approval --project-id <projectId> --task-id <taskId>`
539
547
 
@@ -551,3 +559,5 @@ Audience creation is not a fixed preflight step. For custom task audiences, use
551
559
  `definition_request`. `clientConfig.clientQp` is server-authored and must be omitted from
552
560
  Capability requests; partial updates preserve existing server state. Do not assemble raw QP
553
561
  manually.
562
+ For a `behavior_sequence`, omit second-step `relative_to_first` or set it to `false`; reserve
563
+ `true` for step 3 or later when the window is measured from step 1.
@@ -0,0 +1,61 @@
1
+ # engage-activity.activity-data.detail
2
+
3
+ Query activity delivery trends through the L3 Capability Gateway.
4
+
5
+ Mapped command:
6
+
7
+ ```bash
8
+ ae-cli capability run engage-activity.activity-data.detail --input '<json>'
9
+ ```
10
+
11
+ ## Input
12
+
13
+ Required fields:
14
+
15
+ - `project_id`: project that owns the activity.
16
+ - `activity_id`: activity to query.
17
+ - `start_time`: inclusive start date in `yyyy-MM-dd` format.
18
+ - `end_time`: inclusive end date in `yyyy-MM-dd` format.
19
+
20
+ Optional fields:
21
+
22
+ - `time_particle_size`: `T1` (day), `T2` (week), `T3` (month), or `T5` (total). Defaults to `T1`.
23
+ - `source`: `activity` or `topic_and_task`. Defaults to `activity`.
24
+ - `topic_id_list`: selected topic IDs.
25
+ - `task_id_list`: selected standalone task IDs.
26
+ - `request_id`: cancelable query ID. A UUID is generated when omitted.
27
+
28
+ When `source=topic_and_task` and both ID lists are omitted or empty, the capability selects every topic and standalone task in the activity. When either list is provided, only the explicitly selected resources are queried. Selected resources must belong to the activity and project.
29
+
30
+ ## Recent seven-day topic trend
31
+
32
+ Use an inclusive seven-day range, `T1`, and `topic_and_task`:
33
+
34
+ ```bash
35
+ ae-cli capability run engage-activity.activity-data.detail --input \
36
+ '{"project_id":1,"activity_id":"act-1","start_time":"2026-07-25","end_time":"2026-07-31","time_particle_size":"T1","source":"topic_and_task","request_id":"<uuid>"}'
37
+ ```
38
+
39
+ The report exposes the existing activity-page indicators:
40
+
41
+ - `plan`: planned trigger users.
42
+ - `actualTrigger`: actual push users.
43
+ - `trigger`: successful push users.
44
+
45
+ It does not expose `view` (actual arrival) or `click`. Use the returned header values instead of treating `trigger` as an actual-arrival metric.
46
+
47
+ ## Output
48
+
49
+ Successful output contains:
50
+
51
+ - `data.request_id`: the request ID used by the query.
52
+ - `data.result_generate_time`: ISO-8601 generation time.
53
+ - `data.data.x`: summary/date axis.
54
+ - `data.data.headers`: indicator keys.
55
+ - `data.data.total`: activity totals aligned with `headers`.
56
+ - `data.data.values`: topic or standalone-task rows aligned with `x` and `headers`.
57
+ - `data.data.topic_list`: selected source IDs and names using `topic_id` and `topic_name`.
58
+
59
+ The first `x`/`total` row is the overall summary. For non-total time grains, subsequent rows are the requested date buckets.
60
+
61
+ Use `engage-setting.query.cancel` with the same `request_id` to cancel a running query.