@thinkingai/ae-cli 6.0.48-customer.1 → 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 (144) hide show
  1. package/README.md +96 -36
  2. package/README.zh.md +96 -36
  3. package/dist/{auth-QPEVVBKT.js → auth-FUM37MHF.js} +236 -127
  4. package/dist/{capability-RUSV77CO.js → capability-AXFQW5WM.js} +49 -33
  5. package/dist/{chunk-FR2EDAQM.js → chunk-4P355ZWB.js} +69 -1
  6. package/dist/chunk-6ZIQV5GW.js +26 -0
  7. package/dist/chunk-ATSM5XAW.js +623 -0
  8. package/dist/{chunk-6QK7SYYY.js → chunk-BBEFP4SB.js} +29 -37
  9. package/dist/{chunk-KEWVSO64.js → chunk-CZU3V3DQ.js} +4 -15
  10. package/dist/chunk-ECBLHAZO.js +15 -0
  11. package/dist/{chunk-B5HMQAOM.js → chunk-I4WQAEYB.js} +1 -1
  12. package/dist/{chunk-NSIVXKJH.js → chunk-JDPEVVHR.js} +5 -4
  13. package/dist/{chunk-BE73PUFX.js → chunk-LNZBEQXW.js} +2 -2
  14. package/dist/{chunk-GJXRF7Y6.js → chunk-QJQZH5GQ.js} +47 -78
  15. package/dist/{chunk-BVDST5YT.js → chunk-RSG4ONOI.js} +7 -7
  16. package/dist/{chunk-JYMGHGCE.js → chunk-VXNMYUXU.js} +24 -13
  17. package/dist/{chunk-KZ6ZHF5J.js → chunk-WZ6YVQSF.js} +14 -14
  18. package/dist/{community-report-client-6OL6GBQW.js → community-report-client-XXWGSBSD.js} +2 -4
  19. package/dist/{config-XNMACCKX.js → config-EXUGQN5T.js} +8 -9
  20. package/dist/{data-integration-FZQQC3KW.js → data-integration-4NZ53OMT.js} +7 -9
  21. package/dist/index.js +61 -78
  22. package/dist/{local-data-upload-client-EFHDLXFG.js → local-data-upload-client-KYOKVYZV.js} +2 -4
  23. package/dist/{memory-HBJQ2GC5.js → memory-ATNKZNW5.js} +4 -6
  24. package/dist/{metadata-PGDJE5RQ.js → metadata-SOALPEA7.js} +7 -9
  25. package/dist/{model-BHLJ2QMT.js → model-E4JMQ4V2.js} +4 -6
  26. package/dist/{personal-semantic-preference-E5NZ56DN.js → personal-semantic-preference-LOF33N5X.js} +6 -8
  27. package/dist/project-semantic-RV6NA5LN.js +1113 -0
  28. package/dist/{sync-MUWPRM5I.js → sync-YV3E66IF.js} +6 -8
  29. package/dist/{te-agent-WQZTMEK4.js → te-agent-JB5T3PO7.js} +393 -90
  30. package/dist/{te-analysis-YWCSSLYL.js → te-analysis-TV2YZ4IC.js} +112 -30
  31. package/dist/{te-community-BNE6KREL.js → te-community-UDBI672N.js} +9 -32
  32. package/dist/{te-dataops-TXLKM2W6.js → te-dataops-ZLYOCXZ4.js} +19 -20
  33. package/dist/{te-engage-VEW4CJHK.js → te-engage-EUVMTUDJ.js} +67 -11
  34. package/dist/te-experiment-ESEYE2SK.js +1059 -0
  35. package/dist/{te-kb-BUD6N3NI.js → te-kb-WYQWHFSC.js} +595 -85
  36. package/dist/{te-system-44LZJDTQ.js → te-system-7G6F2LJA.js} +6 -8
  37. package/dist/{te-team-XMP47YXE.js → te-team-E7FBBXMQ.js} +6 -8
  38. package/dist/{update-UODFOZ4P.js → update-D47BUG25.js} +7 -8
  39. package/package.json +13 -9
  40. package/skills/ae-agent/SKILL.md +30 -13
  41. package/skills/ae-agent/references/agent-distribution.md +94 -0
  42. package/skills/ae-agent/references/approval-request.md +4 -0
  43. package/skills/ae-agent/references/command_index.md +9 -2
  44. package/skills/ae-agent/references/create-automation.md +20 -2
  45. package/skills/ae-agent/references/get-agent-context.md +70 -0
  46. package/skills/ae-agent/references/list-automations.md +18 -3
  47. package/skills/ae-agent/references/list-mcp-credentials.md +1 -1
  48. package/skills/ae-agent/references/mcp-token.md +3 -3
  49. package/skills/ae-agent/references/set-mcp-credential.md +0 -1
  50. package/skills/ae-agent/references/update-automation.md +18 -0
  51. package/skills/ae-analysis/SKILL.md +28 -3
  52. package/skills/ae-analysis/references/adhoc_run.md +2 -0
  53. package/skills/ae-analysis/references/ai_models.md +23 -3
  54. package/skills/ae-analysis/references/analysis_gateway_assets.md +2 -2
  55. package/skills/ae-analysis/references/command_index.md +8 -8
  56. package/skills/ae-analysis/references/cross_source_config.md +84 -0
  57. package/skills/ae-analysis/references/dashboard_update.md +1 -1
  58. package/skills/ae-analysis/references/personal_semantic_preference_add.md +1 -1
  59. package/skills/ae-analysis/references/personal_semantic_preference_get.md +3 -1
  60. package/skills/ae-analysis/references/personal_semantic_preference_list.md +3 -1
  61. package/skills/ae-analysis/references/personal_semantic_preference_update.md +1 -1
  62. package/skills/ae-analysis/references/project_role_upsert.md +32 -2
  63. package/skills/ae-analysis/references/user_tag_create.md +23 -1
  64. package/skills/ae-analysis/references/user_tag_models.md +9 -1
  65. package/skills/ae-analysis/references/user_tag_update.md +14 -2
  66. package/skills/ae-data-integration/SKILL.md +2 -1
  67. package/skills/ae-data-integration/references/dimension-routing.md +36 -0
  68. package/skills/ae-data-integration/references/local-analysis.md +2 -0
  69. package/skills/ae-data-integration/references/source-inspect.md +1 -1
  70. package/skills/ae-data-integration/references/ue-routing.md +17 -1
  71. package/skills/ae-engage/SKILL.md +2 -0
  72. package/skills/ae-engage/references/save-flow.md +37 -1
  73. package/skills/ae-experiment/SKILL.md +117 -0
  74. package/skills/ae-experiment/references/batch_delete_experiment.md +11 -0
  75. package/skills/ae-experiment/references/batch_delete_feature.md +11 -0
  76. package/skills/ae-experiment/references/batch_delete_traffic_layer.md +11 -0
  77. package/skills/ae-experiment/references/cancel_experiment_query_by_request_id.md +11 -0
  78. package/skills/ae-experiment/references/check_experiment_ready.md +17 -0
  79. package/skills/ae-experiment/references/check_experiment_traffic_layer_conflict.md +37 -0
  80. package/skills/ae-experiment/references/delete_metric.md +13 -0
  81. package/skills/ae-experiment/references/manage_experiment.md +17 -0
  82. package/skills/ae-experiment/references/manage_feature_status.md +11 -0
  83. package/skills/ae-experiment/references/manage_feature_whitelist.md +66 -0
  84. package/skills/ae-experiment/references/manage_guardrail_metrics.md +26 -0
  85. package/skills/ae-experiment/references/query_bucket_list.md +12 -0
  86. package/skills/ae-experiment/references/query_experiment_detail.md +19 -0
  87. package/skills/ae-experiment/references/query_experiment_list.md +22 -0
  88. package/skills/ae-experiment/references/query_experiment_list_archived.md +18 -0
  89. package/skills/ae-experiment/references/query_experiment_metric_trend.md +12 -0
  90. package/skills/ae-experiment/references/query_experiment_operation_log.md +17 -0
  91. package/skills/ae-experiment/references/query_experiment_report_summary.md +12 -0
  92. package/skills/ae-experiment/references/query_experiment_sample_size_report.md +13 -0
  93. package/skills/ae-experiment/references/query_feature_detail.md +14 -0
  94. package/skills/ae-experiment/references/query_feature_list.md +12 -0
  95. package/skills/ae-experiment/references/query_feature_operation_log.md +19 -0
  96. package/skills/ae-experiment/references/query_feature_version_list.md +20 -0
  97. package/skills/ae-experiment/references/query_metric_detail.md +18 -0
  98. package/skills/ae-experiment/references/query_metric_list.md +15 -0
  99. package/skills/ae-experiment/references/query_traffic_layer_detail.md +13 -0
  100. package/skills/ae-experiment/references/query_traffic_layer_list.md +12 -0
  101. package/skills/ae-experiment/references/save_build_guide.md +39 -0
  102. package/skills/ae-experiment/references/save_experiment.md +210 -0
  103. package/skills/ae-experiment/references/save_feature.md +13 -0
  104. package/skills/ae-experiment/references/save_metric.md +91 -0
  105. package/skills/ae-experiment/references/save_submit_experiment.md +16 -0
  106. package/skills/ae-experiment/references/save_traffic_layer.md +13 -0
  107. package/skills/ae-experiment/references/save_validate.md +33 -0
  108. package/skills/ae-experiment/references/update_experiment_group.md +20 -0
  109. package/skills/ae-experiment-design/SKILL.md +149 -0
  110. package/skills/ae-experiment-design/agents/openai.yaml +4 -0
  111. package/skills/ae-experiment-design/references/client-experiment-sdk.md +147 -0
  112. package/skills/ae-experiment-design/references/experiment-creation.md +108 -0
  113. package/skills/ae-experiment-design/references/experiment-sdk-contract.md +100 -0
  114. package/skills/ae-experiment-design/references/exposure-contract.md +91 -0
  115. package/skills/ae-experiment-design/references/hybrid-experiment-sdk.md +74 -0
  116. package/skills/ae-experiment-design/references/metric-readiness.md +143 -0
  117. package/skills/ae-experiment-design/references/platform-operations.md +105 -0
  118. package/skills/ae-experiment-design/references/sdk-index.md +76 -0
  119. package/skills/ae-experiment-design/references/sdk-integration.md +114 -0
  120. package/skills/ae-experiment-design/references/sdk-troubleshooting.md +139 -0
  121. package/skills/ae-experiment-design/references/server-experiment-sdk.md +78 -0
  122. package/skills/ae-experiment-design/scripts/calculate_experiment_plan.py +450 -0
  123. package/skills/ae-experiment-insight/SKILL.md +149 -0
  124. package/skills/ae-experiment-insight/agents/openai.yaml +4 -0
  125. package/skills/ae-experiment-insight/references/decision-framework.md +69 -0
  126. package/skills/ae-experiment-insight/references/diagnostic-playbook.md +225 -0
  127. package/skills/ae-experiment-insight/references/platform-operations.md +82 -0
  128. package/skills/ae-experiment-insight/scripts/analyze_experiment.py +478 -0
  129. package/skills/ae-generate-tracking-plan/SKILL.md +16 -5
  130. package/skills/ae-kb/SKILL.md +107 -36
  131. package/skills/ae-kb/references/query-workflow.md +59 -29
  132. package/skills/ae-kb/references/versions.md +46 -0
  133. package/skills/ae-project-semantic/SKILL.md +193 -0
  134. package/skills/ae-project-semantic/references/query-routing-v5.md +165 -0
  135. package/skills/ae-project-semantic/references/recommendation-quality.md +68 -0
  136. package/skills/ae-system/SKILL.md +1 -1
  137. package/skills/ae-use-agent/SKILL.md +42 -0
  138. package/skills/ae-use-agent/references/local-agent.md +114 -0
  139. package/dist/auth-UMJAA72O.js +0 -15
  140. package/dist/chunk-533JK3YX.js +0 -72
  141. package/dist/chunk-O6K2ODUE.js +0 -318
  142. package/dist/chunk-RWXIXOZG.js +0 -326
  143. package/dist/client-N7NGZUNI.js +0 -204
  144. package/skills/ae-agent/references/auto-provision-mcp-credentials.md +0 -57
@@ -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())
@@ -111,7 +111,7 @@ If **not** in a sandbox environment, ask exactly:
111
111
  ```text
112
112
  Choose your source material (up to 2):
113
113
 
114
- 1 - Product document (local path, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/URL/images (png/jpg/jpeg/webp)
114
+ 1 - Product document (local path, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/xlsx/pptx/URL/images (png/jpg/jpeg/webp)
115
115
  2 - Detailed description (conversational) — Describe app business flow, core features, user behaviors, monetization model, etc.
116
116
  3 - Codebase (local project path; hidden in sandbox) — Analyze source code to extract events and properties
117
117
  4 - Pre-built template (built-in industry and game genre templates) — Select a built-in template
@@ -126,7 +126,7 @@ If in a sandbox environment, ask exactly:
126
126
  ```text
127
127
  Choose your source material (up to 2):
128
128
 
129
- 1 - Product document (sandbox workspace path, uploaded attachment, URL, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/URL/images (png/jpg/jpeg/webp). You can attach/upload relevant files here.
129
+ 1 - Product document (sandbox workspace path, uploaded attachment, URL, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/xlsx/pptx/URL/images (png/jpg/jpeg/webp). You can attach/upload relevant files here.
130
130
  2 - Detailed description (conversational) — Describe app business flow, core features, user behaviors, monetization model, etc.
131
131
  3 - Pre-built template (built-in industry and game genre templates) — Select a built-in template
132
132
  4 - Modify existing tracking plan (sandbox workspace path) — Import an existing tracking plan xlsx as baseline for modification; can be combined with Product doc / Description, but NOT with Pre-built template
@@ -142,7 +142,7 @@ User can multi-select (max 2). Interpret numbers by the **visible list shown to
142
142
 
143
143
  Canonical source material options (non-sandbox numbering):
144
144
 
145
- 1. **Product document** ****(****local path, sandbox workspace path, uploaded attachment, URL, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/URL/images (png/jpg/jpeg/webp)
145
+ 1. **Product document** ****(****local path, sandbox workspace path, uploaded attachment, URL, image file, or folder) — Extract events and properties from product docs; supports md/pdf/docx/xlsx/pptx/URL/images (png/jpg/jpeg/webp)
146
146
  2. **Detailed description** (conversational) — Describe app business flow, core features, user behaviors, monetization model, etc.
147
147
  3. **Codebase** (local project path; hidden in sandbox) — Analyze source code to extract events and properties
148
148
  4. **Pre-built template** (built-in industry and game genre templates) — Select a built-in template (run `AE_LANG=<user_lang> ae-cli tracking plan list-templates --json` to see available templates)
@@ -674,9 +674,20 @@ Earlier sources take precedence — same-name events keep the earlier version, l
674
674
  - ⚠️ **No model-invented translations for template labels**: When replacing or explaining a localized template-owned label, use the exact value from `src/tracking/i18n` resources. If no corresponding resource exists, preserve the template text and ask the user before changing semantics.
675
675
  - ⚠️ **event_tag is not free-form model translation**: Do not manually map `业务事件`/`系统事件` to another language. Preserve template tags, or rely on `src/tracking/i18n` and autotrack generation for system labels when the CLI owns them.
676
676
  - **codebase**: Scan project source directory, extract events/properties from business logic; **same-name events merge prop_names without overwriting existing fields**; new items `source: "codebase"`
677
- - **prd**: Read all user-provided product documents (md / pdf / docx / URL / images), extract events and properties from each file; **same-name events merge prop_names without overwriting existing fields**; image files analyzed via multimodal interpretation of UI elements and interaction flows; all new items `source: "prd"`
677
+ - **prd**: Read all user-provided product documents (md / pdf / docx / xlsx / pptx / URL / images), extract events and properties from each file; **same-name events merge prop_names without overwriting existing fields**; image files analyzed via multimodal interpretation of UI elements and interaction flows; all new items `source: "prd"`
678
+ - **Read each format with the table below.** Preferred tool first; when it is missing, fall back rather than fail the read.
679
+ | Format | Read via |
680
+ |---|---|
681
+ | md | read directly |
682
+ | docx | `pandoc -t markdown <file>`; fallback `markitdown <file>` (`pip install markitdown` if missing); last resort `unzip -p <file> word/document.xml` and read the text |
683
+ | pdf (text) | extract text (native Read or a PDF text extractor) |
684
+ | pdf (scanned) | render pages to images, then read with vision |
685
+ | xlsx | read rows/columns with a structure-preserving reader (`openpyxl` / `pandas`, `pip install` if missing); a tracking table's row/column layout carries meaning — do NOT rely on a flattened markdown dump |
686
+ | pptx | `markitdown <file>` (`pip install markitdown` if missing) |
687
+ | png/jpg/jpeg/webp | multimodal interpretation, analyze UI elements and interaction flows |
688
+ - **xlsx is a third source-material path, distinct from the two existing xlsx flows.** A human-readable tracking table (event / property / type rows) is read row-by-row here. An **AE-format tracking-plan xlsx** goes through `import-template`; a **CSV/Excel data sample** goes through the data path (`ae-cli data-integration inspect`). Never route one into another's flow.
678
689
  - **prd path is a folder**: Recursively scan all files in the directory:
679
- - md/pdf/docx → read text content, extract events/properties
690
+ - md/pdf/docx/xlsx/pptx → read per the table above, extract events/properties
680
691
  - png/jpg/jpeg/webp → multimodal interpretation, analyze UI elements and interaction flows
681
692
  - subdirectories → recurse
682
693
  - other files → skip