@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.
- package/README.md +4 -1
- package/README.zh.md +4 -1
- package/dist/{capability-J4FU4JZW.js → capability-AXFQW5WM.js} +13 -10
- package/dist/{chunk-6KBBAPPP.js → chunk-JDPEVVHR.js} +4 -3
- package/dist/index.js +23 -13
- package/dist/{metadata-3J5ZAVMF.js → metadata-SOALPEA7.js} +3 -3
- package/dist/{personal-semantic-preference-32AAJ37M.js → personal-semantic-preference-LOF33N5X.js} +2 -2
- package/dist/project-semantic-RV6NA5LN.js +1113 -0
- package/dist/{te-analysis-WHYXHRJD.js → te-analysis-TV2YZ4IC.js} +23 -7
- package/dist/{te-engage-DHKNJ5BO.js → te-engage-EUVMTUDJ.js} +63 -5
- package/dist/te-experiment-ESEYE2SK.js +1059 -0
- package/package.json +2 -1
- package/skills/ae-analysis/SKILL.md +17 -1
- package/skills/ae-analysis/references/command_index.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-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-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/dist/{chunk-HPLNYQPM.js → chunk-RSG4ONOI.js} +3 -3
- package/dist/{sync-2GTAKZYX.js → sync-YV3E66IF.js} +3 -3
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Server Experiment Architecture
|
|
2
|
+
|
|
3
|
+
Read this reference when assignment, Feature evaluation, or behavior execution belongs on a backend service.
|
|
4
|
+
|
|
5
|
+
## Evidence boundary
|
|
6
|
+
|
|
7
|
+
The verified client documentation does not establish an exact AE server experiment SDK, package, endpoint, or method. Do not invent one.
|
|
8
|
+
|
|
9
|
+
Before exact implementation:
|
|
10
|
+
|
|
11
|
+
1. Resolve the real project, experiment, Feature, and assignment unit with `ae-cli`.
|
|
12
|
+
2. Search and inspect the Capability Gateway for server experiment evaluation or SDK generation.
|
|
13
|
+
3. Read verified server documentation if returned by the platform or provided by the user.
|
|
14
|
+
4. Use architecture-level pseudocode until the API contract is verified.
|
|
15
|
+
|
|
16
|
+
## Recommended server contract
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
evaluateExperiment(
|
|
20
|
+
project,
|
|
21
|
+
featureKey,
|
|
22
|
+
stableAssignmentId,
|
|
23
|
+
context
|
|
24
|
+
) -> {
|
|
25
|
+
value,
|
|
26
|
+
valueType,
|
|
27
|
+
assignmentOrGroupId,
|
|
28
|
+
configVersion,
|
|
29
|
+
source,
|
|
30
|
+
evaluatedAt
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The field names above describe an application contract, not a verified AE API schema.
|
|
35
|
+
|
|
36
|
+
## Server responsibilities
|
|
37
|
+
|
|
38
|
+
- derive a stable assignment ID before evaluation;
|
|
39
|
+
- evaluate once per request, session, or product-defined decision boundary;
|
|
40
|
+
- apply a typed control default on timeout or unavailable configuration;
|
|
41
|
+
- preserve assignment consistency across replicas and regions;
|
|
42
|
+
- prevent cache keys from mixing projects, environments, Features, or identities;
|
|
43
|
+
- expose enough internal metadata to diagnose assignment without leaking it to end users;
|
|
44
|
+
- emit or authorize exposure only when treatment becomes effective;
|
|
45
|
+
- report outcomes with a joinable identity.
|
|
46
|
+
|
|
47
|
+
## Cache policy
|
|
48
|
+
|
|
49
|
+
Define separately:
|
|
50
|
+
|
|
51
|
+
- configuration cache;
|
|
52
|
+
- per-identity assignment cache;
|
|
53
|
+
- response or page cache.
|
|
54
|
+
|
|
55
|
+
Do not cache a personalized treatment under a shared response key. Include environment, project, Feature key, and assignment identity in any evaluation cache key unless the verified SDK guarantees safe internal caching.
|
|
56
|
+
|
|
57
|
+
## Failure policy
|
|
58
|
+
|
|
59
|
+
- timeout or transport failure: use typed control default;
|
|
60
|
+
- invalid value type: reject the value and use control;
|
|
61
|
+
- unknown Feature: fail closed to control and alert;
|
|
62
|
+
- stale configuration: use only within the approved stale window;
|
|
63
|
+
- identity missing: either use a documented anonymous assignment or control; never generate a new unstable ID per request.
|
|
64
|
+
|
|
65
|
+
## Exposure ownership
|
|
66
|
+
|
|
67
|
+
If the server fully executes the behavior, server exposure may be appropriate. If the server only sends a variant and the client decides whether it is rendered, client exposure is usually the truthful point.
|
|
68
|
+
|
|
69
|
+
Choose one owner and read `exposure-contract.md`.
|
|
70
|
+
|
|
71
|
+
## Verification checklist
|
|
72
|
+
|
|
73
|
+
- Same identity returns a stable value across replicas.
|
|
74
|
+
- Control fallback is safe and typed.
|
|
75
|
+
- Cache isolation prevents cross-user treatment leakage.
|
|
76
|
+
- Exposure reflects effective behavior, not evaluation alone.
|
|
77
|
+
- Client and server do not both report the same exposure.
|
|
78
|
+
- Outcome identity joins to assignment and exposure.
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Calculate deterministic A/B experiment sample targets and duration."""
|
|
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
|
+
|
|
17
|
+
a = (
|
|
18
|
+
-3.969683028665376e01,
|
|
19
|
+
2.209460984245205e02,
|
|
20
|
+
-2.759285104469687e02,
|
|
21
|
+
1.383577518672690e02,
|
|
22
|
+
-3.066479806614716e01,
|
|
23
|
+
2.506628277459239e00,
|
|
24
|
+
)
|
|
25
|
+
b = (
|
|
26
|
+
-5.447609879822406e01,
|
|
27
|
+
1.615858368580409e02,
|
|
28
|
+
-1.556989798598866e02,
|
|
29
|
+
6.680131188771972e01,
|
|
30
|
+
-1.328068155288572e01,
|
|
31
|
+
)
|
|
32
|
+
c = (
|
|
33
|
+
-7.784894002430293e-03,
|
|
34
|
+
-3.223964580411365e-01,
|
|
35
|
+
-2.400758277161838e00,
|
|
36
|
+
-2.549732539343734e00,
|
|
37
|
+
4.374664141464968e00,
|
|
38
|
+
2.938163982698783e00,
|
|
39
|
+
)
|
|
40
|
+
d = (
|
|
41
|
+
7.784695709041462e-03,
|
|
42
|
+
3.224671290700398e-01,
|
|
43
|
+
2.445134137142996e00,
|
|
44
|
+
3.754408661907416e00,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
low = 0.02425
|
|
48
|
+
high = 1.0 - low
|
|
49
|
+
if probability < low:
|
|
50
|
+
q = math.sqrt(-2.0 * math.log(probability))
|
|
51
|
+
return (
|
|
52
|
+
(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
|
|
53
|
+
/ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
|
|
54
|
+
)
|
|
55
|
+
if probability <= high:
|
|
56
|
+
q = probability - 0.5
|
|
57
|
+
r = q * q
|
|
58
|
+
return (
|
|
59
|
+
(((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5])
|
|
60
|
+
* q
|
|
61
|
+
/ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
|
|
62
|
+
)
|
|
63
|
+
q = math.sqrt(-2.0 * math.log(1.0 - probability))
|
|
64
|
+
return -(
|
|
65
|
+
(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
|
|
66
|
+
/ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def require_number(params: dict[str, Any], key: str) -> float:
|
|
71
|
+
value = params.get(key)
|
|
72
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
73
|
+
raise ValueError(f"{key} must be a number")
|
|
74
|
+
if not math.isfinite(float(value)):
|
|
75
|
+
raise ValueError(f"{key} must be finite")
|
|
76
|
+
return float(value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def optional_integer(params: dict[str, Any], key: str, default: int) -> int:
|
|
80
|
+
value = params.get(key, default)
|
|
81
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
82
|
+
raise ValueError(f"{key} must be an integer")
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def optional_boolean(params: dict[str, Any], key: str, default: bool) -> bool:
|
|
87
|
+
value = params.get(key, default)
|
|
88
|
+
if not isinstance(value, bool):
|
|
89
|
+
raise ValueError(f"{key} must be a boolean")
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def pair_sample_proportion(
|
|
94
|
+
baseline: float,
|
|
95
|
+
treatment: float,
|
|
96
|
+
ratio: float,
|
|
97
|
+
z_alpha: float,
|
|
98
|
+
z_power: float,
|
|
99
|
+
) -> tuple[int, int]:
|
|
100
|
+
delta = abs(treatment - baseline)
|
|
101
|
+
if delta == 0.0:
|
|
102
|
+
raise ValueError("MDE must produce a non-zero absolute effect")
|
|
103
|
+
pooled_alternative = (baseline + ratio * treatment) / (1.0 + ratio)
|
|
104
|
+
first = z_alpha * math.sqrt(
|
|
105
|
+
pooled_alternative * (1.0 - pooled_alternative) * (1.0 + 1.0 / ratio)
|
|
106
|
+
)
|
|
107
|
+
second = z_power * math.sqrt(
|
|
108
|
+
baseline * (1.0 - baseline) + treatment * (1.0 - treatment) / ratio
|
|
109
|
+
)
|
|
110
|
+
control = math.ceil(((first + second) / delta) ** 2)
|
|
111
|
+
return control, math.ceil(control * ratio)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def pair_sample_continuous(
|
|
115
|
+
absolute_effect: float,
|
|
116
|
+
ratio: float,
|
|
117
|
+
z_alpha: float,
|
|
118
|
+
z_power: float,
|
|
119
|
+
control_stddev: float,
|
|
120
|
+
treatment_stddev: float,
|
|
121
|
+
) -> tuple[int, int]:
|
|
122
|
+
if absolute_effect == 0.0:
|
|
123
|
+
raise ValueError("MDE must produce a non-zero absolute effect")
|
|
124
|
+
control = math.ceil(
|
|
125
|
+
(z_alpha + z_power) ** 2
|
|
126
|
+
* (control_stddev**2 + treatment_stddev**2 / ratio)
|
|
127
|
+
/ absolute_effect**2
|
|
128
|
+
)
|
|
129
|
+
return control, math.ceil(control * ratio)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def calculate_duration(
|
|
133
|
+
total_required: int,
|
|
134
|
+
daily_eligible_units: float,
|
|
135
|
+
experiment_traffic: float,
|
|
136
|
+
min_runtime_days: int,
|
|
137
|
+
align_to_full_weeks: bool,
|
|
138
|
+
) -> tuple[float, int, int]:
|
|
139
|
+
"""Return effective daily units, raw duration, and policy-adjusted duration."""
|
|
140
|
+
effective_daily_units = daily_eligible_units * experiment_traffic
|
|
141
|
+
nearest_integer = round(effective_daily_units)
|
|
142
|
+
if math.isclose(
|
|
143
|
+
effective_daily_units,
|
|
144
|
+
nearest_integer,
|
|
145
|
+
rel_tol=0.0,
|
|
146
|
+
abs_tol=1e-9,
|
|
147
|
+
):
|
|
148
|
+
effective_daily_units = float(nearest_integer)
|
|
149
|
+
else:
|
|
150
|
+
effective_daily_units = round(effective_daily_units, 12)
|
|
151
|
+
raw_duration_days = math.ceil(total_required / effective_daily_units)
|
|
152
|
+
duration_days = max(min_runtime_days, raw_duration_days)
|
|
153
|
+
if align_to_full_weeks:
|
|
154
|
+
duration_days = math.ceil(duration_days / 7.0) * 7
|
|
155
|
+
return effective_daily_units, raw_duration_days, duration_days
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def traffic_candidates(params: dict[str, Any], max_available_traffic: float) -> list[float]:
|
|
159
|
+
"""Return validated absolute experiment-traffic candidates."""
|
|
160
|
+
raw_candidates = params.get(
|
|
161
|
+
"traffic_candidates",
|
|
162
|
+
[0.05, 0.10, 0.20, 0.30, 0.50, 0.70, 1.0],
|
|
163
|
+
)
|
|
164
|
+
if not isinstance(raw_candidates, list) or not raw_candidates:
|
|
165
|
+
raise ValueError("traffic_candidates must be a non-empty list")
|
|
166
|
+
|
|
167
|
+
candidates: list[float] = []
|
|
168
|
+
for value in raw_candidates:
|
|
169
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
170
|
+
raise ValueError("every traffic candidate must be a number")
|
|
171
|
+
candidate = float(value)
|
|
172
|
+
if not math.isfinite(candidate) or not 0.0 < candidate <= 1.0:
|
|
173
|
+
raise ValueError("every traffic candidate must be greater than 0 and at most 1")
|
|
174
|
+
if candidate <= max_available_traffic + 1e-12:
|
|
175
|
+
candidates.append(min(candidate, max_available_traffic))
|
|
176
|
+
|
|
177
|
+
candidates.append(max_available_traffic)
|
|
178
|
+
return sorted(set(candidates))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def calculate(params: dict[str, Any]) -> dict[str, Any]:
|
|
182
|
+
metric_type = params.get("metric_type")
|
|
183
|
+
if metric_type not in {"proportion", "continuous"}:
|
|
184
|
+
raise ValueError("metric_type must be proportion or continuous")
|
|
185
|
+
|
|
186
|
+
baseline = require_number(params, "baseline")
|
|
187
|
+
mde = require_number(params, "mde")
|
|
188
|
+
if mde <= 0.0:
|
|
189
|
+
raise ValueError("mde must be greater than 0")
|
|
190
|
+
mde_type = params.get("mde_type")
|
|
191
|
+
if mde_type not in {"relative", "absolute"}:
|
|
192
|
+
raise ValueError("mde_type must be relative or absolute")
|
|
193
|
+
effect_direction = params.get("effect_direction")
|
|
194
|
+
if effect_direction not in {"increase", "decrease"}:
|
|
195
|
+
raise ValueError("effect_direction must be increase or decrease")
|
|
196
|
+
|
|
197
|
+
confidence = (
|
|
198
|
+
require_number(params, "confidence_level")
|
|
199
|
+
if "confidence_level" in params
|
|
200
|
+
else 0.95
|
|
201
|
+
)
|
|
202
|
+
power_source = "provided" if "power" in params else "policy_default"
|
|
203
|
+
power = require_number(params, "power") if "power" in params else 0.80
|
|
204
|
+
if not math.isclose(confidence, 0.95, rel_tol=0.0, abs_tol=1e-12):
|
|
205
|
+
raise ValueError("current experiment reporting requires confidence_level=0.95")
|
|
206
|
+
if not 0.5 < power < 1.0:
|
|
207
|
+
raise ValueError("power must be between 0.5 and 1")
|
|
208
|
+
|
|
209
|
+
allocations_value = params.get("allocations")
|
|
210
|
+
if not isinstance(allocations_value, list) or len(allocations_value) < 2:
|
|
211
|
+
raise ValueError("allocations must contain control and at least one treatment")
|
|
212
|
+
if any(
|
|
213
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
214
|
+
for value in allocations_value
|
|
215
|
+
):
|
|
216
|
+
raise ValueError("every allocation must be a number")
|
|
217
|
+
allocations = [float(value) for value in allocations_value]
|
|
218
|
+
if any(not math.isfinite(value) or value <= 0.0 for value in allocations):
|
|
219
|
+
raise ValueError("every allocation must be a positive finite number")
|
|
220
|
+
if not math.isclose(sum(allocations), 1.0, rel_tol=0.0, abs_tol=1e-9):
|
|
221
|
+
raise ValueError("allocations must sum to 1.0")
|
|
222
|
+
|
|
223
|
+
daily_eligible_units = require_number(params, "daily_eligible_units")
|
|
224
|
+
if daily_eligible_units <= 0.0:
|
|
225
|
+
raise ValueError("daily_eligible_units must be greater than 0")
|
|
226
|
+
|
|
227
|
+
min_runtime_days = optional_integer(params, "min_runtime_days", 7)
|
|
228
|
+
max_runtime_days = optional_integer(params, "max_runtime_days", 60)
|
|
229
|
+
align_to_full_weeks = optional_boolean(params, "align_to_full_weeks", True)
|
|
230
|
+
if min_runtime_days < 1 or max_runtime_days < min_runtime_days:
|
|
231
|
+
raise ValueError("runtime bounds are invalid")
|
|
232
|
+
|
|
233
|
+
number_of_comparisons = len(allocations) - 1
|
|
234
|
+
alpha = 0.05
|
|
235
|
+
multiple_comparison_method = (
|
|
236
|
+
"bonferroni" if number_of_comparisons > 1 else "none"
|
|
237
|
+
)
|
|
238
|
+
alpha_per_comparison = alpha / number_of_comparisons
|
|
239
|
+
z_alpha = normal_ppf(1.0 - alpha_per_comparison / 2.0)
|
|
240
|
+
z_power = normal_ppf(power)
|
|
241
|
+
|
|
242
|
+
absolute_effect = mde if mde_type == "absolute" else abs(baseline) * mde
|
|
243
|
+
signed_effect = absolute_effect if effect_direction == "increase" else -absolute_effect
|
|
244
|
+
treatment_value = baseline + signed_effect
|
|
245
|
+
warnings: list[str] = []
|
|
246
|
+
|
|
247
|
+
if metric_type == "proportion":
|
|
248
|
+
if not 0.0 < baseline < 1.0:
|
|
249
|
+
raise ValueError("proportion baseline must be between 0 and 1")
|
|
250
|
+
if not 0.0 < treatment_value < 1.0:
|
|
251
|
+
raise ValueError("proportion MDE produces a treatment rate outside 0..1")
|
|
252
|
+
else:
|
|
253
|
+
control_stddev = require_number(params, "standard_deviation")
|
|
254
|
+
treatment_stddev = (
|
|
255
|
+
require_number(params, "treatment_standard_deviation")
|
|
256
|
+
if "treatment_standard_deviation" in params
|
|
257
|
+
else control_stddev
|
|
258
|
+
)
|
|
259
|
+
if control_stddev <= 0.0 or treatment_stddev <= 0.0:
|
|
260
|
+
raise ValueError("continuous standard deviations must be greater than 0")
|
|
261
|
+
|
|
262
|
+
control_allocation = allocations[0]
|
|
263
|
+
pair_requirements: list[dict[str, Any]] = []
|
|
264
|
+
total_scale = 0.0
|
|
265
|
+
|
|
266
|
+
for index, treatment_allocation in enumerate(allocations[1:], start=1):
|
|
267
|
+
ratio = treatment_allocation / control_allocation
|
|
268
|
+
if metric_type == "proportion":
|
|
269
|
+
control_required, treatment_required = pair_sample_proportion(
|
|
270
|
+
baseline, treatment_value, ratio, z_alpha, z_power
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
control_required, treatment_required = pair_sample_continuous(
|
|
274
|
+
absolute_effect,
|
|
275
|
+
ratio,
|
|
276
|
+
z_alpha,
|
|
277
|
+
z_power,
|
|
278
|
+
control_stddev,
|
|
279
|
+
treatment_stddev,
|
|
280
|
+
)
|
|
281
|
+
pair_scale = max(
|
|
282
|
+
control_required / control_allocation,
|
|
283
|
+
treatment_required / treatment_allocation,
|
|
284
|
+
)
|
|
285
|
+
total_scale = max(total_scale, pair_scale)
|
|
286
|
+
pair_requirements.append(
|
|
287
|
+
{
|
|
288
|
+
"treatment_index": index,
|
|
289
|
+
"allocation_ratio_to_control": ratio,
|
|
290
|
+
"control_required": control_required,
|
|
291
|
+
"treatment_required": treatment_required,
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
total_required = math.ceil(total_scale)
|
|
296
|
+
group_targets = [math.ceil(total_required * allocation) for allocation in allocations]
|
|
297
|
+
|
|
298
|
+
max_available_traffic: float | None = None
|
|
299
|
+
if "max_available_traffic" in params:
|
|
300
|
+
max_available_traffic = require_number(params, "max_available_traffic")
|
|
301
|
+
if not 0.0 < max_available_traffic <= 1.0:
|
|
302
|
+
raise ValueError("max_available_traffic must be greater than 0 and at most 1")
|
|
303
|
+
|
|
304
|
+
traffic_evaluations: list[dict[str, Any]] = []
|
|
305
|
+
if "experiment_traffic" in params:
|
|
306
|
+
experiment_traffic = require_number(params, "experiment_traffic")
|
|
307
|
+
if not 0.0 < experiment_traffic <= 1.0:
|
|
308
|
+
raise ValueError("experiment_traffic must be greater than 0 and at most 1")
|
|
309
|
+
if (
|
|
310
|
+
max_available_traffic is not None
|
|
311
|
+
and experiment_traffic > max_available_traffic + 1e-12
|
|
312
|
+
):
|
|
313
|
+
raise ValueError("experiment_traffic exceeds max_available_traffic")
|
|
314
|
+
traffic_selection_mode = "provided"
|
|
315
|
+
effective_daily_units, raw_duration_days, duration_days = calculate_duration(
|
|
316
|
+
total_required,
|
|
317
|
+
daily_eligible_units,
|
|
318
|
+
experiment_traffic,
|
|
319
|
+
min_runtime_days,
|
|
320
|
+
align_to_full_weeks,
|
|
321
|
+
)
|
|
322
|
+
traffic_evaluations.append(
|
|
323
|
+
{
|
|
324
|
+
"experiment_traffic": experiment_traffic,
|
|
325
|
+
"effective_daily_units": effective_daily_units,
|
|
326
|
+
"raw_duration_days": raw_duration_days,
|
|
327
|
+
"recommended_duration_days": duration_days,
|
|
328
|
+
"feasible_within_max_runtime": duration_days <= max_runtime_days,
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
if max_available_traffic is None:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
"provide experiment_traffic or max_available_traffic for traffic recommendation"
|
|
335
|
+
)
|
|
336
|
+
traffic_selection_mode = "recommended"
|
|
337
|
+
selected: dict[str, Any] | None = None
|
|
338
|
+
for candidate in traffic_candidates(params, max_available_traffic):
|
|
339
|
+
effective, raw_days, adjusted_days = calculate_duration(
|
|
340
|
+
total_required,
|
|
341
|
+
daily_eligible_units,
|
|
342
|
+
candidate,
|
|
343
|
+
min_runtime_days,
|
|
344
|
+
align_to_full_weeks,
|
|
345
|
+
)
|
|
346
|
+
evaluation = {
|
|
347
|
+
"experiment_traffic": candidate,
|
|
348
|
+
"effective_daily_units": effective,
|
|
349
|
+
"raw_duration_days": raw_days,
|
|
350
|
+
"recommended_duration_days": adjusted_days,
|
|
351
|
+
"feasible_within_max_runtime": adjusted_days <= max_runtime_days,
|
|
352
|
+
}
|
|
353
|
+
traffic_evaluations.append(evaluation)
|
|
354
|
+
if selected is None and evaluation["feasible_within_max_runtime"]:
|
|
355
|
+
selected = evaluation
|
|
356
|
+
if selected is None:
|
|
357
|
+
selected = traffic_evaluations[-1]
|
|
358
|
+
experiment_traffic = float(selected["experiment_traffic"])
|
|
359
|
+
effective_daily_units = float(selected["effective_daily_units"])
|
|
360
|
+
raw_duration_days = int(selected["raw_duration_days"])
|
|
361
|
+
duration_days = int(selected["recommended_duration_days"])
|
|
362
|
+
|
|
363
|
+
feasible_within_max = duration_days <= max_runtime_days
|
|
364
|
+
if not feasible_within_max:
|
|
365
|
+
warnings.append(
|
|
366
|
+
f"Required duration ({duration_days} days) exceeds max_runtime_days ({max_runtime_days})."
|
|
367
|
+
)
|
|
368
|
+
if raw_duration_days < min_runtime_days:
|
|
369
|
+
warnings.append(
|
|
370
|
+
"The statistical target is reached before the minimum runtime; keep the full minimum period."
|
|
371
|
+
)
|
|
372
|
+
if number_of_comparisons > 1:
|
|
373
|
+
warnings.append(
|
|
374
|
+
"Planning uses Bonferroni correction, while the current native report labels significance at unadjusted p < 0.05 with a 95% confidence interval. Use alpha_per_comparison for the preregistered final decision."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
"ok": True,
|
|
379
|
+
"data": {
|
|
380
|
+
"metric_type": metric_type,
|
|
381
|
+
"baseline": baseline,
|
|
382
|
+
"mde": mde,
|
|
383
|
+
"mde_type": mde_type,
|
|
384
|
+
"effect_direction": effect_direction,
|
|
385
|
+
"absolute_effect": absolute_effect,
|
|
386
|
+
"signed_effect": signed_effect,
|
|
387
|
+
"treatment_value": treatment_value,
|
|
388
|
+
"confidence_level": confidence,
|
|
389
|
+
"power": power,
|
|
390
|
+
"power_source": power_source,
|
|
391
|
+
"alpha": alpha,
|
|
392
|
+
"family_alpha": alpha,
|
|
393
|
+
"alpha_per_comparison": alpha_per_comparison,
|
|
394
|
+
"multiple_comparison_method": multiple_comparison_method,
|
|
395
|
+
"number_of_treatment_comparisons": number_of_comparisons,
|
|
396
|
+
"allocations": allocations,
|
|
397
|
+
"group_sample_targets": group_targets,
|
|
398
|
+
"total_sample_target": total_required,
|
|
399
|
+
"pair_requirements": pair_requirements,
|
|
400
|
+
"daily_eligible_units": daily_eligible_units,
|
|
401
|
+
"experiment_traffic": experiment_traffic,
|
|
402
|
+
"selected_experiment_traffic": experiment_traffic,
|
|
403
|
+
"recommended_experiment_traffic": (
|
|
404
|
+
experiment_traffic
|
|
405
|
+
if traffic_selection_mode == "recommended" and feasible_within_max
|
|
406
|
+
else None
|
|
407
|
+
),
|
|
408
|
+
"traffic_selection_mode": traffic_selection_mode,
|
|
409
|
+
"max_available_traffic": max_available_traffic,
|
|
410
|
+
"traffic_candidates_evaluated": traffic_evaluations,
|
|
411
|
+
"effective_daily_units": effective_daily_units,
|
|
412
|
+
"raw_duration_days": raw_duration_days,
|
|
413
|
+
"recommended_duration_days": duration_days,
|
|
414
|
+
"aligned_to_full_weeks": align_to_full_weeks,
|
|
415
|
+
"feasible_within_max_runtime": feasible_within_max,
|
|
416
|
+
"warnings": warnings,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def main() -> int:
|
|
422
|
+
try:
|
|
423
|
+
if len(sys.argv) != 2:
|
|
424
|
+
raise ValueError("usage: calculate_experiment_plan.py '<json-object>'")
|
|
425
|
+
raw = sys.argv[1]
|
|
426
|
+
params = json.load(sys.stdin) if raw == "-" else json.loads(raw)
|
|
427
|
+
if not isinstance(params, dict):
|
|
428
|
+
raise ValueError("input must be a JSON object")
|
|
429
|
+
print(json.dumps(calculate(params), ensure_ascii=False, indent=2))
|
|
430
|
+
return 0
|
|
431
|
+
except (ValueError, KeyError, json.JSONDecodeError) as error:
|
|
432
|
+
print(
|
|
433
|
+
json.dumps(
|
|
434
|
+
{
|
|
435
|
+
"ok": False,
|
|
436
|
+
"error": {
|
|
437
|
+
"type": "validation",
|
|
438
|
+
"code": "INVALID_INPUT",
|
|
439
|
+
"message": str(error),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
ensure_ascii=False,
|
|
443
|
+
indent=2,
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
return 2
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ae-experiment-insight
|
|
3
|
+
description: "Diagnose and interpret AE/TE A/B experiments from configuration and report evidence through a defensible decision. Use when the user asks what an experiment means, whether it can roll out, why a result is not significant, why group sizes or exposure are wrong, why treatment results conflict, whether the report is trustworthy, or what to do next. Covers SRM, duration sufficiency, novelty effects, metric conflicts, missing or anomalous data, design reasonableness, data reliability, metric interpretation, trend and segment analysis, root-cause hypotheses, and rollout recommendations. All platform discovery and reads must use ae-cli."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AE Experiment Insight and Diagnosis
|
|
7
|
+
|
|
8
|
+
Validate the evidence before interpreting the effect. Produce a decision only when the experiment is trustworthy enough to support one.
|
|
9
|
+
|
|
10
|
+
## Platform and reference routing
|
|
11
|
+
|
|
12
|
+
Use `ae-cli` for every AE/TE platform interaction and follow
|
|
13
|
+
[`references/platform-operations.md`](references/platform-operations.md);
|
|
14
|
+
never substitute another platform access path.
|
|
15
|
+
|
|
16
|
+
For diagnosis, read
|
|
17
|
+
[`references/diagnostic-playbook.md`](references/diagnostic-playbook.md).
|
|
18
|
+
Before a rollout decision, read
|
|
19
|
+
[`references/decision-framework.md`](references/decision-framework.md).
|
|
20
|
+
|
|
21
|
+
## Workflow
|
|
22
|
+
|
|
23
|
+
### 1. Restore experiment context
|
|
24
|
+
|
|
25
|
+
Resolve the exact project and experiment. Collect:
|
|
26
|
+
|
|
27
|
+
- hypothesis and intended decision;
|
|
28
|
+
- control and treatment definitions;
|
|
29
|
+
- assignment unit, layer, targeting, traffic, and group allocation;
|
|
30
|
+
- Feature and group values;
|
|
31
|
+
- primary, secondary, and diagnostic metrics;
|
|
32
|
+
- planned MDE, alpha, power, duration, and stopping rule;
|
|
33
|
+
- actual start/end time and material configuration changes.
|
|
34
|
+
|
|
35
|
+
Without the original hypothesis or success rule, explain observed effects but do not retroactively invent success criteria.
|
|
36
|
+
|
|
37
|
+
### 2. Check run integrity
|
|
38
|
+
|
|
39
|
+
Establish the experiment's actual state, complete runtime periods, and material
|
|
40
|
+
mid-run Feature, traffic, audience, metric, or identity changes. Treat
|
|
41
|
+
configuration history as evidence. Evaluate allocation, exposure, sample
|
|
42
|
+
sufficiency, and design quality through the diagnostic playbook rather than
|
|
43
|
+
duplicating its rules here.
|
|
44
|
+
|
|
45
|
+
### 3. Run data diagnostics
|
|
46
|
+
|
|
47
|
+
Apply `diagnostic-playbook.md` in causal order and complete its diagnostic
|
|
48
|
+
checklist. Use
|
|
49
|
+
[`scripts/analyze_experiment.py`](scripts/analyze_experiment.py) for SRM and
|
|
50
|
+
group comparisons; do not calculate p-values or confidence intervals mentally.
|
|
51
|
+
|
|
52
|
+
### 4. Interpret effects
|
|
53
|
+
|
|
54
|
+
For each pre-registered metric, interpret the verified metric contract from
|
|
55
|
+
the diagnostic playbook and report:
|
|
56
|
+
|
|
57
|
+
- verified source event, aggregation, analysis unit, denominator or population,
|
|
58
|
+
filters, and attribution window;
|
|
59
|
+
- control and treatment values;
|
|
60
|
+
- absolute and relative difference;
|
|
61
|
+
- confidence interval and p-value from deterministic calculation or the verified platform report;
|
|
62
|
+
- achieved sample versus planned sample;
|
|
63
|
+
- relation to MDE and business threshold;
|
|
64
|
+
- statistical and practical significance.
|
|
65
|
+
|
|
66
|
+
Distinguish:
|
|
67
|
+
|
|
68
|
+
- statistically significant and practically valuable;
|
|
69
|
+
- statistically significant but too small to matter;
|
|
70
|
+
- directionally positive but underpowered;
|
|
71
|
+
- no detectable effect within the current precision;
|
|
72
|
+
- significantly negative;
|
|
73
|
+
- invalid or inconclusive because of data quality.
|
|
74
|
+
|
|
75
|
+
Do not translate `p > alpha` into “no effect.” State that the current data did not establish an effect and describe the compatible interval.
|
|
76
|
+
|
|
77
|
+
### 5. Analyze trends and segments
|
|
78
|
+
|
|
79
|
+
Check trend stability and abnormal dates using complete comparable periods.
|
|
80
|
+
Apply the trend and segment rules in `decision-framework.md`.
|
|
81
|
+
|
|
82
|
+
### 6. Diagnose causes
|
|
83
|
+
|
|
84
|
+
For every suspected cause, provide:
|
|
85
|
+
|
|
86
|
+
- observed evidence;
|
|
87
|
+
- inference and uncertainty;
|
|
88
|
+
- competing explanation;
|
|
89
|
+
- the exact platform query or product check that would distinguish them;
|
|
90
|
+
- remediation if confirmed.
|
|
91
|
+
|
|
92
|
+
Do not produce a list of generic causes detached from evidence.
|
|
93
|
+
In the final report, express the distinguishing check as a concise user-facing
|
|
94
|
+
verification action. Include the raw `ae-cli` command only when the user
|
|
95
|
+
explicitly asks for commands, an audit trail, or debugging details.
|
|
96
|
+
|
|
97
|
+
### 7. Make the decision
|
|
98
|
+
|
|
99
|
+
Apply `decision-framework.md` and return one decision supported by the trust
|
|
100
|
+
assessment, primary effect, duration, trends, segments, and diagnostic results.
|
|
101
|
+
|
|
102
|
+
Never start, pause, end, change traffic, delete, or roll out an experiment unless the user separately asks for that platform action. Those actions are outside interpretation and require explicit target-and-impact confirmation.
|
|
103
|
+
|
|
104
|
+
## Output requirements
|
|
105
|
+
|
|
106
|
+
- Use the explicitly requested language, otherwise the language of the user's latest substantive message. Localize all user-facing prose, headings, labels, statuses, conclusions, warnings, limitations, and next actions; treat section names in this Skill as semantic guidance and remove unintended mixed-language output.
|
|
107
|
+
- Keep code, commands, raw IDs, event/property/metric names, Feature keys, SDK/API names, and official enum values unchanged when translation would alter their technical meaning.
|
|
108
|
+
- Lead with the decision status.
|
|
109
|
+
- Use an adaptive report, not a fixed numbered template. Organize the default
|
|
110
|
+
user-facing hierarchy as: conclusion, core impact, experiment decision, and
|
|
111
|
+
product optimization or next action.
|
|
112
|
+
- Keep data reliability, core metrics, and attention metrics inside the
|
|
113
|
+
**core impact** section:
|
|
114
|
+
- **Data reliability validation**: summarize the diagnostic checklist and
|
|
115
|
+
state the overall result as reliable, limited, or unreliable. Keep normal
|
|
116
|
+
checks compact and expand only warnings, failures, or unverified checks.
|
|
117
|
+
- **Core metrics**: present the primary metric's control and treatment
|
|
118
|
+
values, absolute and relative effect, interval and p-value, relation to MDE
|
|
119
|
+
or the business threshold, and business meaning. Use causal wording only
|
|
120
|
+
when the trust assessment supports it.
|
|
121
|
+
- **Attention metrics**: present secondary and diagnostic metrics only when
|
|
122
|
+
they affect the decision, explain the mechanism, or reveal material risk.
|
|
123
|
+
Do not promote a post-hoc metric into the success criterion.
|
|
124
|
+
- For a brief answer, collapse these subsections into compact paragraphs under
|
|
125
|
+
core impact; do not promote them into unrelated top-level sections.
|
|
126
|
+
- Treat labels such as `material blockers`, `trust blockers`, diagnostic
|
|
127
|
+
severity names, and framework labels such as `Gate 1` or `Gate 2` as
|
|
128
|
+
internal taxonomy. Never expose them as report headings or append their
|
|
129
|
+
English forms after localized headings. Use natural
|
|
130
|
+
user-facing wording such as “数据可靠性验证” or “当前为什么不能下结论” when
|
|
131
|
+
those concepts need a heading.
|
|
132
|
+
- Place metric trends and metric-related segments under the relevant core or
|
|
133
|
+
attention metric. Add broader context, cause diagnosis, assumptions, or
|
|
134
|
+
limitations only when they materially change interpretation.
|
|
135
|
+
- Use a product-optimization section only when trustworthy evidence supports a
|
|
136
|
+
specific product change. Otherwise use a next-validation or next-action
|
|
137
|
+
section.
|
|
138
|
+
- Include only content supported by the request and evidence. Do not add a section merely because it appeared in a previous report.
|
|
139
|
+
- Separate platform observations, deterministic calculations, and inferred causes. State uncertainty and evidence gaps explicitly.
|
|
140
|
+
- Do not return an unexplained raw table.
|
|
141
|
+
|
|
142
|
+
## Failure behavior
|
|
143
|
+
|
|
144
|
+
- Ambiguous project or experiment: show resolved candidates and ask; do not guess.
|
|
145
|
+
- Missing report capability: preserve the `ae-cli` gap and request a platform export with required fields.
|
|
146
|
+
- Permission failure: stop dependent queries and report the missing permission.
|
|
147
|
+
- Empty successful report: say no matching data; do not relabel it as a transport failure.
|
|
148
|
+
- Partial success: use available evidence and enumerate failures.
|
|
149
|
+
- Conflicting sources: prefer raw assignment/exposure evidence for data-quality diagnosis and explain the discrepancy.
|