@thinkingai/ae-cli 6.0.48 → 6.1.0-project-role-upsert.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +4 -1
  2. package/README.zh.md +4 -1
  3. package/dist/{capability-J4FU4JZW.js → capability-AXFQW5WM.js} +13 -10
  4. package/dist/{chunk-6KBBAPPP.js → chunk-JDPEVVHR.js} +4 -3
  5. package/dist/index.js +23 -13
  6. package/dist/{metadata-3J5ZAVMF.js → metadata-SOALPEA7.js} +3 -3
  7. package/dist/{personal-semantic-preference-32AAJ37M.js → personal-semantic-preference-LOF33N5X.js} +2 -2
  8. package/dist/project-semantic-RV6NA5LN.js +1113 -0
  9. package/dist/{te-analysis-WHYXHRJD.js → te-analysis-TV2YZ4IC.js} +23 -7
  10. package/dist/{te-engage-DHKNJ5BO.js → te-engage-EUVMTUDJ.js} +63 -5
  11. package/dist/te-experiment-ESEYE2SK.js +1059 -0
  12. package/package.json +2 -1
  13. package/skills/ae-analysis/SKILL.md +17 -1
  14. package/skills/ae-analysis/references/command_index.md +1 -1
  15. package/skills/ae-analysis/references/personal_semantic_preference_add.md +1 -1
  16. package/skills/ae-analysis/references/personal_semantic_preference_get.md +3 -1
  17. package/skills/ae-analysis/references/personal_semantic_preference_list.md +3 -1
  18. package/skills/ae-analysis/references/personal_semantic_preference_update.md +1 -1
  19. package/skills/ae-analysis/references/project_role_upsert.md +32 -2
  20. package/skills/ae-engage/SKILL.md +2 -0
  21. package/skills/ae-engage/references/save-flow.md +37 -1
  22. package/skills/ae-experiment/SKILL.md +117 -0
  23. package/skills/ae-experiment/references/batch_delete_experiment.md +11 -0
  24. package/skills/ae-experiment/references/batch_delete_feature.md +11 -0
  25. package/skills/ae-experiment/references/batch_delete_traffic_layer.md +11 -0
  26. package/skills/ae-experiment/references/cancel_experiment_query_by_request_id.md +11 -0
  27. package/skills/ae-experiment/references/check_experiment_ready.md +17 -0
  28. package/skills/ae-experiment/references/check_experiment_traffic_layer_conflict.md +37 -0
  29. package/skills/ae-experiment/references/delete_metric.md +13 -0
  30. package/skills/ae-experiment/references/manage_experiment.md +17 -0
  31. package/skills/ae-experiment/references/manage_feature_status.md +11 -0
  32. package/skills/ae-experiment/references/manage_feature_whitelist.md +66 -0
  33. package/skills/ae-experiment/references/manage_guardrail_metrics.md +26 -0
  34. package/skills/ae-experiment/references/query_bucket_list.md +12 -0
  35. package/skills/ae-experiment/references/query_experiment_detail.md +19 -0
  36. package/skills/ae-experiment/references/query_experiment_list.md +22 -0
  37. package/skills/ae-experiment/references/query_experiment_list_archived.md +18 -0
  38. package/skills/ae-experiment/references/query_experiment_metric_trend.md +12 -0
  39. package/skills/ae-experiment/references/query_experiment_operation_log.md +17 -0
  40. package/skills/ae-experiment/references/query_experiment_report_summary.md +12 -0
  41. package/skills/ae-experiment/references/query_experiment_sample_size_report.md +13 -0
  42. package/skills/ae-experiment/references/query_feature_detail.md +14 -0
  43. package/skills/ae-experiment/references/query_feature_list.md +12 -0
  44. package/skills/ae-experiment/references/query_feature_operation_log.md +19 -0
  45. package/skills/ae-experiment/references/query_feature_version_list.md +20 -0
  46. package/skills/ae-experiment/references/query_metric_detail.md +18 -0
  47. package/skills/ae-experiment/references/query_metric_list.md +15 -0
  48. package/skills/ae-experiment/references/query_traffic_layer_detail.md +13 -0
  49. package/skills/ae-experiment/references/query_traffic_layer_list.md +12 -0
  50. package/skills/ae-experiment/references/save_build_guide.md +39 -0
  51. package/skills/ae-experiment/references/save_experiment.md +210 -0
  52. package/skills/ae-experiment/references/save_feature.md +13 -0
  53. package/skills/ae-experiment/references/save_metric.md +91 -0
  54. package/skills/ae-experiment/references/save_submit_experiment.md +16 -0
  55. package/skills/ae-experiment/references/save_traffic_layer.md +13 -0
  56. package/skills/ae-experiment/references/save_validate.md +33 -0
  57. package/skills/ae-experiment/references/update_experiment_group.md +20 -0
  58. package/skills/ae-experiment-design/SKILL.md +149 -0
  59. package/skills/ae-experiment-design/agents/openai.yaml +4 -0
  60. package/skills/ae-experiment-design/references/client-experiment-sdk.md +147 -0
  61. package/skills/ae-experiment-design/references/experiment-creation.md +108 -0
  62. package/skills/ae-experiment-design/references/experiment-sdk-contract.md +100 -0
  63. package/skills/ae-experiment-design/references/exposure-contract.md +91 -0
  64. package/skills/ae-experiment-design/references/hybrid-experiment-sdk.md +74 -0
  65. package/skills/ae-experiment-design/references/metric-readiness.md +143 -0
  66. package/skills/ae-experiment-design/references/platform-operations.md +105 -0
  67. package/skills/ae-experiment-design/references/sdk-index.md +76 -0
  68. package/skills/ae-experiment-design/references/sdk-integration.md +114 -0
  69. package/skills/ae-experiment-design/references/sdk-troubleshooting.md +139 -0
  70. package/skills/ae-experiment-design/references/server-experiment-sdk.md +78 -0
  71. package/skills/ae-experiment-design/scripts/calculate_experiment_plan.py +450 -0
  72. package/skills/ae-experiment-insight/SKILL.md +149 -0
  73. package/skills/ae-experiment-insight/agents/openai.yaml +4 -0
  74. package/skills/ae-experiment-insight/references/decision-framework.md +69 -0
  75. package/skills/ae-experiment-insight/references/diagnostic-playbook.md +225 -0
  76. package/skills/ae-experiment-insight/references/platform-operations.md +82 -0
  77. package/skills/ae-experiment-insight/scripts/analyze_experiment.py +478 -0
  78. package/skills/ae-project-semantic/SKILL.md +193 -0
  79. package/skills/ae-project-semantic/references/query-routing-v5.md +165 -0
  80. package/skills/ae-project-semantic/references/recommendation-quality.md +68 -0
  81. package/dist/{chunk-HPLNYQPM.js → chunk-RSG4ONOI.js} +3 -3
  82. package/dist/{sync-2GTAKZYX.js → sync-YV3E66IF.js} +3 -3
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env python3
2
+ """Calculate deterministic SRM and treatment-versus-control comparisons."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import math
8
+ import sys
9
+ from typing import Any
10
+
11
+
12
+ def normal_ppf(probability: float) -> float:
13
+ """Return the inverse standard-normal CDF using Acklam's approximation."""
14
+ if not 0.0 < probability < 1.0:
15
+ raise ValueError("probability must be between 0 and 1")
16
+ a = (
17
+ -3.969683028665376e01,
18
+ 2.209460984245205e02,
19
+ -2.759285104469687e02,
20
+ 1.383577518672690e02,
21
+ -3.066479806614716e01,
22
+ 2.506628277459239e00,
23
+ )
24
+ b = (
25
+ -5.447609879822406e01,
26
+ 1.615858368580409e02,
27
+ -1.556989798598866e02,
28
+ 6.680131188771972e01,
29
+ -1.328068155288572e01,
30
+ )
31
+ c = (
32
+ -7.784894002430293e-03,
33
+ -3.223964580411365e-01,
34
+ -2.400758277161838e00,
35
+ -2.549732539343734e00,
36
+ 4.374664141464968e00,
37
+ 2.938163982698783e00,
38
+ )
39
+ d = (
40
+ 7.784695709041462e-03,
41
+ 3.224671290700398e-01,
42
+ 2.445134137142996e00,
43
+ 3.754408661907416e00,
44
+ )
45
+ low = 0.02425
46
+ high = 1.0 - low
47
+ if probability < low:
48
+ q = math.sqrt(-2.0 * math.log(probability))
49
+ return (
50
+ (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
51
+ / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
52
+ )
53
+ if probability <= high:
54
+ q = probability - 0.5
55
+ r = q * q
56
+ return (
57
+ (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5])
58
+ * q
59
+ / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
60
+ )
61
+ q = math.sqrt(-2.0 * math.log(1.0 - probability))
62
+ return -(
63
+ (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
64
+ / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
65
+ )
66
+
67
+
68
+ def regularized_gamma_q(shape: float, value: float) -> float:
69
+ """Return the regularized upper incomplete gamma function Q(shape, value)."""
70
+ if shape <= 0.0 or value < 0.0:
71
+ raise ValueError("invalid gamma arguments")
72
+ if value == 0.0:
73
+ return 1.0
74
+ epsilon = 3e-14
75
+ minimum = 1e-300
76
+ max_iterations = 10000
77
+ log_gamma = math.lgamma(shape)
78
+
79
+ if value < shape + 1.0:
80
+ term = 1.0 / shape
81
+ total = term
82
+ current = shape
83
+ for _ in range(max_iterations):
84
+ current += 1.0
85
+ term *= value / current
86
+ total += term
87
+ if abs(term) < abs(total) * epsilon:
88
+ lower = total * math.exp(-value + shape * math.log(value) - log_gamma)
89
+ return max(0.0, min(1.0, 1.0 - lower))
90
+ raise ValueError("gamma series did not converge")
91
+
92
+ b = value + 1.0 - shape
93
+ c = 1.0 / minimum
94
+ d = 1.0 / b
95
+ h = d
96
+ for iteration in range(1, max_iterations + 1):
97
+ coefficient = -iteration * (iteration - shape)
98
+ b += 2.0
99
+ d = coefficient * d + b
100
+ if abs(d) < minimum:
101
+ d = minimum
102
+ c = b + coefficient / c
103
+ if abs(c) < minimum:
104
+ c = minimum
105
+ d = 1.0 / d
106
+ delta = d * c
107
+ h *= delta
108
+ if abs(delta - 1.0) < epsilon:
109
+ result = math.exp(-value + shape * math.log(value) - log_gamma) * h
110
+ return max(0.0, min(1.0, result))
111
+ raise ValueError("gamma continued fraction did not converge")
112
+
113
+
114
+ def beta_continued_fraction(a: float, b: float, x: float) -> float:
115
+ max_iterations = 10000
116
+ epsilon = 3e-14
117
+ minimum = 1e-300
118
+ qab = a + b
119
+ qap = a + 1.0
120
+ qam = a - 1.0
121
+ c = 1.0
122
+ d = 1.0 - qab * x / qap
123
+ if abs(d) < minimum:
124
+ d = minimum
125
+ d = 1.0 / d
126
+ h = d
127
+ for iteration in range(1, max_iterations + 1):
128
+ even = 2 * iteration
129
+ coefficient = iteration * (b - iteration) * x / ((qam + even) * (a + even))
130
+ d = 1.0 + coefficient * d
131
+ if abs(d) < minimum:
132
+ d = minimum
133
+ c = 1.0 + coefficient / c
134
+ if abs(c) < minimum:
135
+ c = minimum
136
+ d = 1.0 / d
137
+ h *= d * c
138
+
139
+ coefficient = -(a + iteration) * (qab + iteration) * x / (
140
+ (a + even) * (qap + even)
141
+ )
142
+ d = 1.0 + coefficient * d
143
+ if abs(d) < minimum:
144
+ d = minimum
145
+ c = 1.0 + coefficient / c
146
+ if abs(c) < minimum:
147
+ c = minimum
148
+ d = 1.0 / d
149
+ delta = d * c
150
+ h *= delta
151
+ if abs(delta - 1.0) < epsilon:
152
+ return h
153
+ raise ValueError("beta continued fraction did not converge")
154
+
155
+
156
+ def regularized_beta(a: float, b: float, x: float) -> float:
157
+ if a <= 0.0 or b <= 0.0 or not 0.0 <= x <= 1.0:
158
+ raise ValueError("invalid beta arguments")
159
+ if x == 0.0:
160
+ return 0.0
161
+ if x == 1.0:
162
+ return 1.0
163
+ front = math.exp(
164
+ math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b)
165
+ + a * math.log(x) + b * math.log(1.0 - x)
166
+ )
167
+ if x < (a + 1.0) / (a + b + 2.0):
168
+ return front * beta_continued_fraction(a, b, x) / a
169
+ return 1.0 - front * beta_continued_fraction(b, a, 1.0 - x) / b
170
+
171
+
172
+ def student_t_two_sided_p(t_value: float, degrees_freedom: float) -> float:
173
+ x = degrees_freedom / (degrees_freedom + t_value * t_value)
174
+ return regularized_beta(degrees_freedom / 2.0, 0.5, x)
175
+
176
+
177
+ def student_t_critical(alpha: float, degrees_freedom: float) -> float:
178
+ low = 0.0
179
+ high = 1.0
180
+ while student_t_two_sided_p(high, degrees_freedom) > alpha:
181
+ high *= 2.0
182
+ if high > 1e6:
183
+ raise ValueError("could not bracket t critical value")
184
+ for _ in range(100):
185
+ middle = (low + high) / 2.0
186
+ if student_t_two_sided_p(middle, degrees_freedom) > alpha:
187
+ low = middle
188
+ else:
189
+ high = middle
190
+ return (low + high) / 2.0
191
+
192
+
193
+ def validate_groups(params: dict[str, Any]) -> list[dict[str, Any]]:
194
+ groups = params.get("groups")
195
+ if not isinstance(groups, list) or len(groups) < 2:
196
+ raise ValueError("groups must contain at least two groups")
197
+ names: set[str] = set()
198
+ for group in groups:
199
+ if not isinstance(group, dict):
200
+ raise ValueError("every group must be an object")
201
+ name = group.get("name")
202
+ if not isinstance(name, str) or not name:
203
+ raise ValueError("every group must have a non-empty name")
204
+ if name in names:
205
+ raise ValueError(f"duplicate group name: {name}")
206
+ names.add(name)
207
+ return groups
208
+
209
+
210
+ def calculate_srm(
211
+ groups: list[dict[str, Any]],
212
+ expected_allocations: dict[str, Any],
213
+ alpha_srm: float,
214
+ ) -> dict[str, Any]:
215
+ names = [group["name"] for group in groups]
216
+ if set(expected_allocations) != set(names):
217
+ raise ValueError("expected_allocations keys must exactly match group names")
218
+ allocations = {name: float(expected_allocations[name]) for name in names}
219
+ if any(value <= 0.0 for value in allocations.values()):
220
+ raise ValueError("expected allocations must be positive")
221
+ if not math.isclose(sum(allocations.values()), 1.0, rel_tol=0.0, abs_tol=1e-9):
222
+ raise ValueError("expected allocations must sum to 1.0")
223
+
224
+ observed: dict[str, int] = {}
225
+ for group in groups:
226
+ assigned = group.get("assigned")
227
+ if isinstance(assigned, bool) or not isinstance(assigned, int) or assigned < 0:
228
+ raise ValueError("every group must have a non-negative integer assigned count")
229
+ observed[group["name"]] = assigned
230
+ total = sum(observed.values())
231
+ if total == 0:
232
+ raise ValueError("total assigned count must be greater than 0")
233
+
234
+ expected = {name: total * allocations[name] for name in names}
235
+ chi_square = sum(
236
+ (observed[name] - expected[name]) ** 2 / expected[name] for name in names
237
+ )
238
+ degrees_freedom = len(names) - 1
239
+ p_value = regularized_gamma_q(degrees_freedom / 2.0, chi_square / 2.0)
240
+ return {
241
+ "chi_square": chi_square,
242
+ "degrees_freedom": degrees_freedom,
243
+ "p_value": p_value,
244
+ "alpha": alpha_srm,
245
+ "srm_detected": p_value < alpha_srm,
246
+ "observed": observed,
247
+ "expected": expected,
248
+ }
249
+
250
+
251
+ def binary_comparison(
252
+ control: dict[str, Any],
253
+ treatment: dict[str, Any],
254
+ alpha: float,
255
+ ) -> dict[str, Any]:
256
+ n0 = int(control.get("sample_size", 0))
257
+ n1 = int(treatment.get("sample_size", 0))
258
+ x0 = int(control.get("successes", -1))
259
+ x1 = int(treatment.get("successes", -1))
260
+ if n0 <= 0 or n1 <= 0:
261
+ raise ValueError("binary sample_size must be greater than 0")
262
+ if not 0 <= x0 <= n0 or not 0 <= x1 <= n1:
263
+ raise ValueError("binary successes must be between 0 and sample_size")
264
+ p0 = x0 / n0
265
+ p1 = x1 / n1
266
+ difference = p1 - p0
267
+ pooled = (x0 + x1) / (n0 + n1)
268
+ pooled_se = math.sqrt(pooled * (1.0 - pooled) * (1.0 / n0 + 1.0 / n1))
269
+ if pooled_se == 0.0:
270
+ z_value = 0.0 if difference == 0.0 else math.copysign(math.inf, difference)
271
+ p_value = 1.0 if difference == 0.0 else 0.0
272
+ else:
273
+ z_value = difference / pooled_se
274
+ p_value = math.erfc(abs(z_value) / math.sqrt(2.0))
275
+ interval_se = math.sqrt(
276
+ p0 * (1.0 - p0) / n0 + p1 * (1.0 - p1) / n1
277
+ )
278
+ critical = normal_ppf(1.0 - alpha / 2.0)
279
+ relative_lift = None if p0 == 0.0 else difference / p0
280
+ return {
281
+ "control_value": p0,
282
+ "treatment_value": p1,
283
+ "absolute_difference": difference,
284
+ "relative_lift": relative_lift,
285
+ "test": "two_sided_pooled_two_proportion_z",
286
+ "statistic": z_value,
287
+ "p_value": p_value,
288
+ "confidence_interval_absolute": [
289
+ difference - critical * interval_se,
290
+ difference + critical * interval_se,
291
+ ],
292
+ }
293
+
294
+
295
+ def continuous_comparison(
296
+ control: dict[str, Any],
297
+ treatment: dict[str, Any],
298
+ alpha: float,
299
+ ) -> dict[str, Any]:
300
+ n0 = int(control.get("sample_size", 0))
301
+ n1 = int(treatment.get("sample_size", 0))
302
+ mean0 = float(control.get("mean"))
303
+ mean1 = float(treatment.get("mean"))
304
+ sd0 = float(control.get("standard_deviation"))
305
+ sd1 = float(treatment.get("standard_deviation"))
306
+ if n0 < 2 or n1 < 2:
307
+ raise ValueError("continuous sample_size must be at least 2")
308
+ if sd0 < 0.0 or sd1 < 0.0:
309
+ raise ValueError("standard_deviation must be non-negative")
310
+ variance0 = sd0 * sd0 / n0
311
+ variance1 = sd1 * sd1 / n1
312
+ standard_error = math.sqrt(variance0 + variance1)
313
+ difference = mean1 - mean0
314
+ if standard_error == 0.0:
315
+ t_value = 0.0 if difference == 0.0 else math.copysign(math.inf, difference)
316
+ p_value = 1.0 if difference == 0.0 else 0.0
317
+ degrees_freedom = math.inf
318
+ critical = normal_ppf(1.0 - alpha / 2.0)
319
+ else:
320
+ t_value = difference / standard_error
321
+ denominator = (
322
+ variance0 * variance0 / (n0 - 1)
323
+ + variance1 * variance1 / (n1 - 1)
324
+ )
325
+ degrees_freedom = (
326
+ math.inf if denominator == 0.0 else (variance0 + variance1) ** 2 / denominator
327
+ )
328
+ if math.isinf(degrees_freedom):
329
+ p_value = math.erfc(abs(t_value) / math.sqrt(2.0))
330
+ critical = normal_ppf(1.0 - alpha / 2.0)
331
+ else:
332
+ p_value = student_t_two_sided_p(t_value, degrees_freedom)
333
+ critical = student_t_critical(alpha, degrees_freedom)
334
+ relative_lift = None if mean0 == 0.0 else difference / mean0
335
+ return {
336
+ "control_value": mean0,
337
+ "treatment_value": mean1,
338
+ "absolute_difference": difference,
339
+ "relative_lift": relative_lift,
340
+ "test": "two_sided_welch_t",
341
+ "statistic": t_value,
342
+ "degrees_freedom": degrees_freedom,
343
+ "p_value": p_value,
344
+ "confidence_interval_absolute": [
345
+ difference - critical * standard_error,
346
+ difference + critical * standard_error,
347
+ ],
348
+ }
349
+
350
+
351
+ def calculate(params: dict[str, Any]) -> dict[str, Any]:
352
+ groups = validate_groups(params)
353
+ metric_type = params.get("metric_type")
354
+ if metric_type not in {"binary", "continuous"}:
355
+ raise ValueError("metric_type must be binary or continuous")
356
+ control_name = params.get("control")
357
+ by_name = {group["name"]: group for group in groups}
358
+ if control_name not in by_name:
359
+ raise ValueError("control must match one group name")
360
+
361
+ alpha = float(params.get("alpha", 0.05))
362
+ alpha_srm = float(params.get("alpha_srm", 0.01))
363
+ if not 0.0 < alpha < 0.5 or not 0.0 < alpha_srm < 0.5:
364
+ raise ValueError("alpha and alpha_srm must be between 0 and 0.5")
365
+ multiple_testing = params.get("multiple_testing", "bonferroni")
366
+ if multiple_testing not in {"bonferroni", "none"}:
367
+ raise ValueError("multiple_testing must be bonferroni or none")
368
+
369
+ srm = None
370
+ expected_allocations = params.get("expected_allocations")
371
+ if expected_allocations is not None:
372
+ if not isinstance(expected_allocations, dict):
373
+ raise ValueError("expected_allocations must be an object")
374
+ srm = calculate_srm(groups, expected_allocations, alpha_srm)
375
+
376
+ control = by_name[control_name]
377
+ comparisons: list[dict[str, Any]] = []
378
+ number_of_comparisons = len(groups) - 1
379
+ comparison_alpha = (
380
+ alpha / number_of_comparisons
381
+ if multiple_testing == "bonferroni"
382
+ else alpha
383
+ )
384
+ for treatment in groups:
385
+ if treatment["name"] == control_name:
386
+ continue
387
+ result = (
388
+ binary_comparison(control, treatment, comparison_alpha)
389
+ if metric_type == "binary"
390
+ else continuous_comparison(control, treatment, comparison_alpha)
391
+ )
392
+ difference = result["absolute_difference"]
393
+ significant = result["p_value"] < comparison_alpha
394
+ if significant and difference > 0:
395
+ classification = "statistically_significant_positive"
396
+ elif significant and difference < 0:
397
+ classification = "statistically_significant_negative"
398
+ else:
399
+ classification = "inconclusive_at_selected_alpha"
400
+ result.update(
401
+ {
402
+ "control": control_name,
403
+ "treatment": treatment["name"],
404
+ "comparison_alpha": comparison_alpha,
405
+ "statistically_significant": significant,
406
+ "classification": classification,
407
+ }
408
+ )
409
+ comparisons.append(result)
410
+
411
+ exposure_coverage: dict[str, float] = {}
412
+ for group in groups:
413
+ assigned = group.get("assigned")
414
+ sample_size = group.get("sample_size")
415
+ if (
416
+ isinstance(assigned, int)
417
+ and assigned > 0
418
+ and isinstance(sample_size, int)
419
+ and sample_size >= 0
420
+ ):
421
+ exposure_coverage[group["name"]] = sample_size / assigned
422
+
423
+ warnings: list[str] = []
424
+ if len(comparisons) > 1 and multiple_testing == "none":
425
+ warnings.append(
426
+ "P-values are unadjusted across treatments; confirm that this matches the pre-registered policy."
427
+ )
428
+ if srm and srm["srm_detected"]:
429
+ warnings.append(
430
+ "SRM was detected from assignment counts; diagnose allocation before using causal comparisons."
431
+ )
432
+
433
+ return {
434
+ "ok": True,
435
+ "data": {
436
+ "metric_type": metric_type,
437
+ "control": control_name,
438
+ "familywise_alpha": alpha,
439
+ "multiple_testing": multiple_testing,
440
+ "comparison_alpha": comparison_alpha,
441
+ "srm": srm,
442
+ "exposure_coverage": exposure_coverage,
443
+ "comparisons": comparisons,
444
+ "warnings": warnings,
445
+ },
446
+ }
447
+
448
+
449
+ def main() -> int:
450
+ try:
451
+ if len(sys.argv) != 2:
452
+ raise ValueError("usage: analyze_experiment.py '<json-object>'")
453
+ raw = sys.argv[1]
454
+ params = json.load(sys.stdin) if raw == "-" else json.loads(raw)
455
+ if not isinstance(params, dict):
456
+ raise ValueError("input must be a JSON object")
457
+ print(json.dumps(calculate(params), ensure_ascii=False, indent=2, allow_nan=False))
458
+ return 0
459
+ except (ValueError, TypeError, KeyError, json.JSONDecodeError) as error:
460
+ print(
461
+ json.dumps(
462
+ {
463
+ "ok": False,
464
+ "error": {
465
+ "type": "validation",
466
+ "code": "INVALID_INPUT",
467
+ "message": str(error),
468
+ },
469
+ },
470
+ ensure_ascii=False,
471
+ indent=2,
472
+ )
473
+ )
474
+ return 2
475
+
476
+
477
+ if __name__ == "__main__":
478
+ raise SystemExit(main())
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: ae-project-semantic
3
+ description: "Use when generating, testing, submitting, reviewing, or publishing governed project semantic candidates from AE/TE project asset packages. This skill owns progressive asset-scope consent, recommendation quality gates, evidence authority, topic-domain grouping, candidate JSON generation, CLI closed-loop validation, and frontend review acceptance. Do not use it for ordinary analysis questions that only consume already published semantics."
4
+ ---
5
+
6
+ # ae-project-semantic
7
+
8
+ Project semantics are governed L2 project-wide business concepts, rules, calculation conventions, and default asset-selection methods. They sit above dashboards and reports. Dashboards, reports, events, properties, and metrics are L1 evidence and execution assets; they are not project semantics by themselves.
9
+
10
+ Use `ae-cli` only. Do not write the database directly for recommendations, approvals, or releases.
11
+
12
+ ## Boundary
13
+
14
+ - Use this skill to generate or evaluate project semantic recommendations.
15
+ - Use `ae-analysis` to consume already published project semantics during analysis tasks.
16
+ - Frontend can review, edit, approve, reject, and publish candidates; it must not generate recommendations.
17
+ - Start with governed authenticated dashboards/reports. Broader project assets may be inspected only after explicit user consent and must retain their lower authority in evidence and confidence. Events, properties, metrics, background documents, and notes can provide supporting evidence only, never the primary L2 candidate subject.
18
+
19
+ ## Required Workflow
20
+
21
+ 1. Resolve the project and host.
22
+ 2. Read both recommendation references before inspecting candidates:
23
+ - [`references/recommendation-quality.md`](references/recommendation-quality.md) for the governed L2 quality bar;
24
+ - [`references/query-routing-v5.md`](references/query-routing-v5.md) for the current query-routing, recall-shortcut, and analysis-playbook discovery protocol.
25
+ 3. Check existing published project semantics:
26
+
27
+ ```bash
28
+ ae-cli project-semantic list --project-id <project_id>
29
+ ```
30
+
31
+ 4. Export governed authenticated project assets and semantic snapshot context by default:
32
+
33
+ ```bash
34
+ ae-cli project-semantic asset-package export --project-id <project_id> --asset-scope governed --output <tmp>/project-semantic-assets --force
35
+ ```
36
+
37
+ When `--host` points directly to a locally started Common service instead of the deployed analysis gateway, scope the empty gateway domain to that command so the request uses Common's native `/api/cli/v1` route. Do not export this override globally:
38
+
39
+ ```bash
40
+ AE_CLI_CAPABILITY_GATEWAY_DOMAIN= ae-cli project-semantic asset-package export --project-id <project_id> --asset-scope governed --output <tmp>/project-semantic-assets --force --host http://127.0.0.1:8992
41
+ ```
42
+
43
+ 5. Inspect `asset_scope`, `exported_asset_count`, `authenticated_asset_count`, `unauthenticated_asset_count`, `truncated`, work-unit count, and definition-family count before scanning. If `truncated=true`, warn that the package hit a compatibility limit. If the governed package is too sparse to support useful problem frames, report the actual counts and ask whether to re-export active collaborative assets. Do not broaden automatically:
44
+
45
+ ```bash
46
+ ae-cli project-semantic asset-package export --project-id <project_id> --asset-scope collaborative --output <tmp>/project-semantic-assets --force
47
+ ```
48
+
49
+ If that package is still too sparse, report its counts and ask separately whether to export all valid project dashboards/reports and their referenced metadata:
50
+
51
+ ```bash
52
+ ae-cli project-semantic asset-package export --project-id <project_id> --asset-scope all_visible --output <tmp>/project-semantic-assets --force
53
+ ```
54
+
55
+ `governed` is authenticated + collaborative + active in the recent 90-day window. `collaborative` removes only the authentication requirement. `all_visible` removes authentication, collaboration, and recency filters while still excluding deleted, frozen, hidden, offline, or otherwise invalid assets. Export is project-administrator-only; no per-user visibility filtering is required inside `all_visible`. Asset authentication is a separate governance write and must never be changed implicitly by scanning.
56
+
57
+ 6. Read the package in this order:
58
+ - `manifest.json`, `.asset-package.json`, `catalog/published.jsonl`, `catalog/disabled.jsonl` when present, `catalog/active-candidates.jsonl`, and `catalog/rejected-candidates.jsonl` to build the exclusion and revision context;
59
+ - `indexes/work-units.jsonl` as the dashboard-first investigation queue, preserving its usage-priority order while rotating across distinct business themes;
60
+ - `indexes/definition-families.jsonl` and compact dashboard/report records to compare reusable definition variants;
61
+ - `indexes/asset-directory.jsonl` and `indexes/governance-coverage.jsonl` for supporting evidence;
62
+ - `details/normalized/**` only for candidate-bearing or conflicting families, and `details/raw/**` only when the normalized definition is insufficient.
63
+
64
+ The service owns deterministic parsing, key-field normalization, definition signatures, usage ordering, and catalog assembly. The Agent owns business topic discovery, evidence interpretation, counterexample review, and candidate wording. Display names, descriptions, and notes remain evidence for interpretation, but they never make two otherwise identical definitions distinct. Do not load every detail file into context up front or delegate business judgment to a project-specific keyword table or deterministic template generator.
65
+
66
+ 7. Compare the proposed behavior against published, disabled, active-candidate, and rejected catalogs. Disabled semantics are suppression context: do not consume, recreate, or update them through recommendation generation. A project administrator can explicitly enable them later. Produce `CREATE` only for a genuinely new semantic, `UPDATE` when an active semantic needs a material revision, and no candidate when the package is already covered. Zero new candidates is a valid successful recommendation result.
67
+
68
+ 8. Validate the Agent-authored file against the exact exported package. This command checks only deterministic contract rules such as required fields, supported enums, catalog conflicts, duplicate fingerprints, and resolvable evidence references. `passed=true` is not a semantic-quality approval:
69
+
70
+ ```bash
71
+ ae-cli project-semantic candidate validate --asset-package <tmp>/project-semantic-assets --submit-file <tmp>/project-semantic-candidates.json
72
+ ```
73
+
74
+ 9. Perform a separate Agent quality-review pass over the unchanged candidate file and the evidence it cites. Apply the Hard Quality Bar and `references/recommendation-quality.md`; do not rely on the generation pass to approve its own wording. For every candidate, record `PASS`, `REVISE`, or `INSUFFICIENT_EVIDENCE` with concrete findings and evidence references. Revise and repeat both deterministic validation and Agent review until every submitted candidate is `PASS`. A successful empty recommendation is preferable to weak filler.
75
+
76
+ 10. Submit the unchanged validated and Agent-reviewed file. Use `.asset-package.json.snapshot_hash`, or the `snapshot_hash` returned by `candidate validate`:
77
+
78
+ ```bash
79
+ ae-cli project-semantic candidate submit --project-id <project_id> --submit-file <tmp>/project-semantic-candidates.json --snapshot-hash <snapshot_hash>
80
+ ```
81
+
82
+ 11. Review and enable through CLI when validating the full closed loop:
83
+
84
+ ```bash
85
+ ae-cli project-semantic candidate list --project-id <project_id>
86
+ ae-cli project-semantic candidate get --project-id <project_id> --candidate-id <candidate_id>
87
+ ae-cli project-semantic candidate enable --project-id <project_id> --candidate-ids '["candidate_1"]'
88
+ ae-cli project-semantic get --project-id <project_id> --id <semantic_id> --mark-used
89
+ ```
90
+
91
+ Lifecycle management is deliberately separate from recommendation review:
92
+
93
+ ```bash
94
+ ae-cli project-semantic disable --project-id <project_id> --semantic-id <semantic_id> --expected-version <version> --reason <reason>
95
+ ae-cli project-semantic list --project-id <project_id> --status disabled
96
+ ae-cli project-semantic delete-impact --project-id <project_id> --semantic-id <semantic_id>
97
+ ae-cli project-semantic delete --project-id <project_id> --semantic-id <semantic_id> --expected-version <version> --reason <reason>
98
+ ae-cli project-semantic enable --project-id <project_id> --semantic-id <semantic_id> --expected-version <version> --reason <reason>
99
+ ```
100
+
101
+ Disable means “do not consume and do not recommend again.” From disabled state, either enable the same semantic or inspect impact and physically delete it. Physical deletion means the semantic is absent and may be rediscovered by a future scan. Never delete merely to regenerate recommendations.
102
+
103
+ ## Hard Quality Bar
104
+
105
+ A candidate file is not acceptable unless all of these are true:
106
+
107
+ - It starts from governed evidence, or records the explicitly approved broader scope and lowers confidence for claims that depend on unauthenticated assets.
108
+ - It groups by L2 business topic domain before generating candidates.
109
+ - Every topic group includes the submit-contract fields `topic_domain_key`, `topic_domain_title`, `topic_group_key`, and `topic_group_title`.
110
+ - It does not create one candidate per asset.
111
+ - It does not promote an event/property/metric name into a project semantic title.
112
+ - Each candidate binds `resource_refs` with real asset names/types/ids or names from the package.
113
+ - Each candidate explains what it means, where it applies, how to use it, and what it excludes.
114
+ - Each candidate body covers business definition, applicable questions, decision or calculation rules, Agent usage, and boundaries or exceptions in the project's natural language. Exact headings are not a machine contract.
115
+ - The concrete business judgment belongs in the decision or calculation content; asset names and counts belong in `resource_refs`, `evidence`, and `recommendation_reason`, not in the semantic body.
116
+ - A batch fails review when candidates reuse the same body with only asset names or counts changed.
117
+ - Each candidate includes evidence excerpts or source titles that show why it was recommended.
118
+ - Each candidate changes how the Agent interprets a business question, selects default assets, or applies a project-wide rule.
119
+ - Each candidate declares one of the evidence-backed recommendation kinds in `query-routing-v5.md`: `QUERY_ROUTING`, `RECALL_SHORTCUT`, or `ANALYSIS_PLAYBOOK`.
120
+ - A formula, threshold, state, event, property, report, or asset name is supporting L1 evidence, never a standalone candidate.
121
+ - A route must contain a meaningful asset-choice branch; a shortcut must contain a minimal bundle plus stop and fallback; a playbook must be note-backed or explicitly labeled as a structure-inferred recommended path.
122
+ - Existing published and disabled semantics are checked before submission; disabled semantics suppress recreation.
123
+ - An unauthenticated asset may support a route, shortcut, or playbook only when definitions, work-unit structure, usage, or notes provide corroboration. Never present it as a certified project rule solely because it was exported.
124
+ - Sparse evidence is either skipped or marked lower confidence; do not inflate weak recommendations.
125
+
126
+ ## Generic Extraction Protocol
127
+
128
+ The protocol must work unchanged for games, retail, finance, SaaS, operations, and unknown project domains.
129
+
130
+ 1. Inventory evidence without deciding themes. Separate authenticated and unauthenticated dashboards/reports, record the export scope and selection reason, and keep existing governed semantics and supporting L1 metadata distinct.
131
+ 2. Discover semantic families only from evidence that was actually read. Use these cross-domain dimensions as questions, not as prefilled answers:
132
+ - business object, state, and alias;
133
+ - analysis subject and deduplication grain;
134
+ - measure, aggregation, numerator, denominator, and transformation;
135
+ - inclusion, exclusion, and filter scope;
136
+ - query range, cohort window, observation window, and freshness;
137
+ - unit, currency, normalization, and attribution;
138
+ - default asset selection and relationships between assets;
139
+ - applicability, exception, and conflict boundaries.
140
+ 3. Compare definition variants inside each family. A candidate may state a default only when evidence shows one authoritative variant. Conflicting variants require a disambiguation rule or a warning, not an invented standard.
141
+ 4. Form a topic domain after the semantic families are understood. The domain title must come from the project's natural business language and explain why its candidates are reviewed together.
142
+ 5. Generate a candidate only when removing it would make a future Agent more likely to choose the wrong business object, asset, grain, formula, scope, unit, window, or exception.
143
+ 6. For every claim in the body, identify direct supporting evidence. Keep asset names and excerpts in `resource_refs`, `evidence`, and `recommendation_reason`; keep reusable business knowledge in `content`.
144
+ 7. Compare candidates by resulting Agent behavior. Merge candidates that lead to the same interpretation and execution; split candidates that answer independently searchable questions.
145
+ 8. Run a counterexample pass: inspect minority definitions and assets that may violate the proposed rule. Narrow or drop unsupported conclusions.
146
+ 9. Before wording candidates, form evidence-backed problem frames and assign asset roles. Use `query-routing-v5.md`; do not optimize for candidate count or one-candidate-per-asset coverage.
147
+ 10. Keep a decision ledger for discovered problem frames, including routed, shortcut, playbook, definition-only, and evidence-insufficient dispositions.
148
+
149
+ ## Anti-Overfitting Rule
150
+
151
+ - Never encode a customer, project name, project ID, asset ID, expected topic title, expected candidate title, or expected candidate count in this skill or CLI generation code.
152
+ - Never start from a fixed industry taxonomy or keyword dictionary. Keywords may locate evidence only after a semantic family is discovered; they must not determine the result.
153
+ - A test package is an evaluation sample, not a source of reusable rules. Tune the protocol only when the change is defensible across unrelated domains.
154
+ - Quality is measured by evidence support and changed Agent behavior, not by matching a previously approved list of topics.
155
+
156
+ ## User-Facing Recommendation Display
157
+
158
+ When showing CLI recommendation results to a user, never flatten candidates into a single numbered list. The user-facing answer must use the same hierarchy as the candidate JSON and frontend review UI:
159
+
160
+ ```text
161
+ Project semantic recommendation scan completed
162
+
163
+ Summary:
164
+ - Topic domains: <N>
165
+ - Candidate semantics: <N>
166
+ - Validation: passed | failed
167
+
168
+ Topic domain: <topic_domain_title> (<candidate_count> semantics)
169
+ - [<semantic_type>] <candidate title>
170
+ <one-sentence summary>
171
+ Evidence: <asset title 1>, <asset title 2>, ...
172
+ ```
173
+
174
+ Rules:
175
+
176
+ - Show topic domains first, then candidates under each domain.
177
+ - Include each domain's semantic count.
178
+ - Include each candidate's semantic type, title, short summary, and primary evidence asset titles.
179
+ - If the CLI command returns `topic_groups`, use that field directly for the display order and counts.
180
+ - Keep the full JSON path or submit command separate from the human summary.
181
+ - Do not present a flat list like `1. semantic A 2. semantic B ...` unless the user explicitly asks for raw candidate order.
182
+
183
+ ## Output Discipline
184
+
185
+ When reporting recommendation results, include:
186
+
187
+ - commands run;
188
+ - asset scope, exported/authenticated/unauthenticated asset counts, and snapshot hash;
189
+ - topic domains, semantic counts per domain, and candidate titles grouped under each domain;
190
+ - evidence assets per candidate;
191
+ - quality warnings, if any;
192
+ - problem-frame coverage and rejected shallow-opportunity warnings;
193
+ - submit/review/release IDs after writes.