@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.
- package/README.md +96 -36
- package/README.zh.md +96 -36
- package/dist/{auth-QPEVVBKT.js → auth-FUM37MHF.js} +236 -127
- package/dist/{capability-RUSV77CO.js → capability-AXFQW5WM.js} +49 -33
- package/dist/{chunk-FR2EDAQM.js → chunk-4P355ZWB.js} +69 -1
- package/dist/chunk-6ZIQV5GW.js +26 -0
- package/dist/chunk-ATSM5XAW.js +623 -0
- package/dist/{chunk-6QK7SYYY.js → chunk-BBEFP4SB.js} +29 -37
- package/dist/{chunk-KEWVSO64.js → chunk-CZU3V3DQ.js} +4 -15
- package/dist/chunk-ECBLHAZO.js +15 -0
- package/dist/{chunk-B5HMQAOM.js → chunk-I4WQAEYB.js} +1 -1
- package/dist/{chunk-NSIVXKJH.js → chunk-JDPEVVHR.js} +5 -4
- package/dist/{chunk-BE73PUFX.js → chunk-LNZBEQXW.js} +2 -2
- package/dist/{chunk-GJXRF7Y6.js → chunk-QJQZH5GQ.js} +47 -78
- package/dist/{chunk-BVDST5YT.js → chunk-RSG4ONOI.js} +7 -7
- package/dist/{chunk-JYMGHGCE.js → chunk-VXNMYUXU.js} +24 -13
- package/dist/{chunk-KZ6ZHF5J.js → chunk-WZ6YVQSF.js} +14 -14
- package/dist/{community-report-client-6OL6GBQW.js → community-report-client-XXWGSBSD.js} +2 -4
- package/dist/{config-XNMACCKX.js → config-EXUGQN5T.js} +8 -9
- package/dist/{data-integration-FZQQC3KW.js → data-integration-4NZ53OMT.js} +7 -9
- package/dist/index.js +61 -78
- package/dist/{local-data-upload-client-EFHDLXFG.js → local-data-upload-client-KYOKVYZV.js} +2 -4
- package/dist/{memory-HBJQ2GC5.js → memory-ATNKZNW5.js} +4 -6
- package/dist/{metadata-PGDJE5RQ.js → metadata-SOALPEA7.js} +7 -9
- package/dist/{model-BHLJ2QMT.js → model-E4JMQ4V2.js} +4 -6
- package/dist/{personal-semantic-preference-E5NZ56DN.js → personal-semantic-preference-LOF33N5X.js} +6 -8
- package/dist/project-semantic-RV6NA5LN.js +1113 -0
- package/dist/{sync-MUWPRM5I.js → sync-YV3E66IF.js} +6 -8
- package/dist/{te-agent-WQZTMEK4.js → te-agent-JB5T3PO7.js} +393 -90
- package/dist/{te-analysis-YWCSSLYL.js → te-analysis-TV2YZ4IC.js} +112 -30
- package/dist/{te-community-BNE6KREL.js → te-community-UDBI672N.js} +9 -32
- package/dist/{te-dataops-TXLKM2W6.js → te-dataops-ZLYOCXZ4.js} +19 -20
- package/dist/{te-engage-VEW4CJHK.js → te-engage-EUVMTUDJ.js} +67 -11
- package/dist/te-experiment-ESEYE2SK.js +1059 -0
- package/dist/{te-kb-BUD6N3NI.js → te-kb-WYQWHFSC.js} +595 -85
- package/dist/{te-system-44LZJDTQ.js → te-system-7G6F2LJA.js} +6 -8
- package/dist/{te-team-XMP47YXE.js → te-team-E7FBBXMQ.js} +6 -8
- package/dist/{update-UODFOZ4P.js → update-D47BUG25.js} +7 -8
- package/package.json +13 -9
- package/skills/ae-agent/SKILL.md +30 -13
- package/skills/ae-agent/references/agent-distribution.md +94 -0
- package/skills/ae-agent/references/approval-request.md +4 -0
- package/skills/ae-agent/references/command_index.md +9 -2
- package/skills/ae-agent/references/create-automation.md +20 -2
- package/skills/ae-agent/references/get-agent-context.md +70 -0
- package/skills/ae-agent/references/list-automations.md +18 -3
- package/skills/ae-agent/references/list-mcp-credentials.md +1 -1
- package/skills/ae-agent/references/mcp-token.md +3 -3
- package/skills/ae-agent/references/set-mcp-credential.md +0 -1
- package/skills/ae-agent/references/update-automation.md +18 -0
- package/skills/ae-analysis/SKILL.md +28 -3
- package/skills/ae-analysis/references/adhoc_run.md +2 -0
- package/skills/ae-analysis/references/ai_models.md +23 -3
- package/skills/ae-analysis/references/analysis_gateway_assets.md +2 -2
- package/skills/ae-analysis/references/command_index.md +8 -8
- package/skills/ae-analysis/references/cross_source_config.md +84 -0
- package/skills/ae-analysis/references/dashboard_update.md +1 -1
- package/skills/ae-analysis/references/personal_semantic_preference_add.md +1 -1
- package/skills/ae-analysis/references/personal_semantic_preference_get.md +3 -1
- package/skills/ae-analysis/references/personal_semantic_preference_list.md +3 -1
- package/skills/ae-analysis/references/personal_semantic_preference_update.md +1 -1
- package/skills/ae-analysis/references/project_role_upsert.md +32 -2
- package/skills/ae-analysis/references/user_tag_create.md +23 -1
- package/skills/ae-analysis/references/user_tag_models.md +9 -1
- package/skills/ae-analysis/references/user_tag_update.md +14 -2
- package/skills/ae-data-integration/SKILL.md +2 -1
- package/skills/ae-data-integration/references/dimension-routing.md +36 -0
- package/skills/ae-data-integration/references/local-analysis.md +2 -0
- package/skills/ae-data-integration/references/source-inspect.md +1 -1
- package/skills/ae-data-integration/references/ue-routing.md +17 -1
- package/skills/ae-engage/SKILL.md +2 -0
- package/skills/ae-engage/references/save-flow.md +37 -1
- package/skills/ae-experiment/SKILL.md +117 -0
- package/skills/ae-experiment/references/batch_delete_experiment.md +11 -0
- package/skills/ae-experiment/references/batch_delete_feature.md +11 -0
- package/skills/ae-experiment/references/batch_delete_traffic_layer.md +11 -0
- package/skills/ae-experiment/references/cancel_experiment_query_by_request_id.md +11 -0
- package/skills/ae-experiment/references/check_experiment_ready.md +17 -0
- package/skills/ae-experiment/references/check_experiment_traffic_layer_conflict.md +37 -0
- package/skills/ae-experiment/references/delete_metric.md +13 -0
- package/skills/ae-experiment/references/manage_experiment.md +17 -0
- package/skills/ae-experiment/references/manage_feature_status.md +11 -0
- package/skills/ae-experiment/references/manage_feature_whitelist.md +66 -0
- package/skills/ae-experiment/references/manage_guardrail_metrics.md +26 -0
- package/skills/ae-experiment/references/query_bucket_list.md +12 -0
- package/skills/ae-experiment/references/query_experiment_detail.md +19 -0
- package/skills/ae-experiment/references/query_experiment_list.md +22 -0
- package/skills/ae-experiment/references/query_experiment_list_archived.md +18 -0
- package/skills/ae-experiment/references/query_experiment_metric_trend.md +12 -0
- package/skills/ae-experiment/references/query_experiment_operation_log.md +17 -0
- package/skills/ae-experiment/references/query_experiment_report_summary.md +12 -0
- package/skills/ae-experiment/references/query_experiment_sample_size_report.md +13 -0
- package/skills/ae-experiment/references/query_feature_detail.md +14 -0
- package/skills/ae-experiment/references/query_feature_list.md +12 -0
- package/skills/ae-experiment/references/query_feature_operation_log.md +19 -0
- package/skills/ae-experiment/references/query_feature_version_list.md +20 -0
- package/skills/ae-experiment/references/query_metric_detail.md +18 -0
- package/skills/ae-experiment/references/query_metric_list.md +15 -0
- package/skills/ae-experiment/references/query_traffic_layer_detail.md +13 -0
- package/skills/ae-experiment/references/query_traffic_layer_list.md +12 -0
- package/skills/ae-experiment/references/save_build_guide.md +39 -0
- package/skills/ae-experiment/references/save_experiment.md +210 -0
- package/skills/ae-experiment/references/save_feature.md +13 -0
- package/skills/ae-experiment/references/save_metric.md +91 -0
- package/skills/ae-experiment/references/save_submit_experiment.md +16 -0
- package/skills/ae-experiment/references/save_traffic_layer.md +13 -0
- package/skills/ae-experiment/references/save_validate.md +33 -0
- package/skills/ae-experiment/references/update_experiment_group.md +20 -0
- package/skills/ae-experiment-design/SKILL.md +149 -0
- package/skills/ae-experiment-design/agents/openai.yaml +4 -0
- package/skills/ae-experiment-design/references/client-experiment-sdk.md +147 -0
- package/skills/ae-experiment-design/references/experiment-creation.md +108 -0
- package/skills/ae-experiment-design/references/experiment-sdk-contract.md +100 -0
- package/skills/ae-experiment-design/references/exposure-contract.md +91 -0
- package/skills/ae-experiment-design/references/hybrid-experiment-sdk.md +74 -0
- package/skills/ae-experiment-design/references/metric-readiness.md +143 -0
- package/skills/ae-experiment-design/references/platform-operations.md +105 -0
- package/skills/ae-experiment-design/references/sdk-index.md +76 -0
- package/skills/ae-experiment-design/references/sdk-integration.md +114 -0
- package/skills/ae-experiment-design/references/sdk-troubleshooting.md +139 -0
- package/skills/ae-experiment-design/references/server-experiment-sdk.md +78 -0
- package/skills/ae-experiment-design/scripts/calculate_experiment_plan.py +450 -0
- package/skills/ae-experiment-insight/SKILL.md +149 -0
- package/skills/ae-experiment-insight/agents/openai.yaml +4 -0
- package/skills/ae-experiment-insight/references/decision-framework.md +69 -0
- package/skills/ae-experiment-insight/references/diagnostic-playbook.md +225 -0
- package/skills/ae-experiment-insight/references/platform-operations.md +82 -0
- package/skills/ae-experiment-insight/scripts/analyze_experiment.py +478 -0
- package/skills/ae-generate-tracking-plan/SKILL.md +16 -5
- package/skills/ae-kb/SKILL.md +107 -36
- package/skills/ae-kb/references/query-workflow.md +59 -29
- package/skills/ae-kb/references/versions.md +46 -0
- package/skills/ae-project-semantic/SKILL.md +193 -0
- package/skills/ae-project-semantic/references/query-routing-v5.md +165 -0
- package/skills/ae-project-semantic/references/recommendation-quality.md +68 -0
- package/skills/ae-system/SKILL.md +1 -1
- package/skills/ae-use-agent/SKILL.md +42 -0
- package/skills/ae-use-agent/references/local-agent.md +114 -0
- package/dist/auth-UMJAA72O.js +0 -15
- package/dist/chunk-533JK3YX.js +0 -72
- package/dist/chunk-O6K2ODUE.js +0 -318
- package/dist/chunk-RWXIXOZG.js +0 -326
- package/dist/client-N7NGZUNI.js +0 -204
- 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
|
|
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
|