skilldrop-cli 0.16.30 → 0.16.32
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/bin/skilldrop.js +133 -18
- package/catalogue.json +4 -2
- package/contracts/skill.schema.json +3 -3
- package/model-routing.json +8 -0
- package/package.json +1 -1
- package/packs/converters/skills/mermaid-render/manifest.json +11 -1
- package/packs/dev-team/loops/release/LOOP.md +7 -5
- package/packs/dev-team/loops/release/loop.json +2 -2
- package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +2 -2
- package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +2 -2
- package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +52 -34
- package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +33 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/ci.yml +39 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/.github/workflows/integration.yml +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/PR.md +39 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/go.mod +17 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/notify/client.go +35 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client.go +66 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_integration_test.go +38 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/client_test.go +53 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/internal/payments/settle.go +30 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/pr.diff +42 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/vendor-notes/httpkit-CHANGELOG.md +43 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.github/workflows/ci.yml +67 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.lintwright.json +7 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/.nvmrc +1 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/PR.md +110 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/package.json +25 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/pr.diff +146 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/due-date.ts +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/src/totals.ts +11 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/due-date.test.ts +17 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/tests/totals.test.ts +12 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +23 -14
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +17 -5
- package/packs/dev-team/skills/dependency-upgrade-review/reference.md +36 -1
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +22 -4
- package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
- package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
- package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
- package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
- package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
- package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
- package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
- package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
- package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
- package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
- package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
- package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
- package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
- package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
- package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
- package/packs/dev-team/skills/sonar-review/manifest.json +12 -0
- package/packs/infra-as-code/skills/terraform-module/manifest.json +11 -0
- package/packs/infra-as-code/skills/terraform-plan-review/manifest.json +8 -1
- package/packs/solution-architect/skills/figma-diagrams/manifest.json +12 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""How long each exposure step of a canary or flag rollout must run before its
|
|
3
|
+
numbers can detect the regression you care about.
|
|
4
|
+
|
|
5
|
+
Uses the normal approximation to the two-proportion z-test. The flag-on cohort
|
|
6
|
+
(treatment) is compared with a concurrent flag-off cohort (control). By default
|
|
7
|
+
the control is everyone not exposed (100% - exposure); pass --control-pct to fix
|
|
8
|
+
it at a holdback size instead.
|
|
9
|
+
|
|
10
|
+
For each step it prints the units (requests or users) each arm needs and the
|
|
11
|
+
hours that takes at the given traffic, flags steps that cannot reach that in
|
|
12
|
+
--max-hours, says the smallest regression the step *can* detect in that window,
|
|
13
|
+
and suggests the smallest exposure that reaches sufficiency in the window.
|
|
14
|
+
|
|
15
|
+
Rates and regressions accept a fraction (0.008), a percent ("0.8%") or, for an
|
|
16
|
+
absolute regression, percentage points ("0.3pp").
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python3 canary_math.py --baseline 0.8% --detect-abs 0.3pp --traffic 9000
|
|
20
|
+
python3 canary_math.py --baseline 3.1% --detect-rel 10% --direction down \\
|
|
21
|
+
--traffic 9000 --steps 1,5,25,50 --unit users
|
|
22
|
+
python3 canary_math.py ... --json
|
|
23
|
+
|
|
24
|
+
Exit codes: 0 printed, 2 bad input.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import math
|
|
30
|
+
import sys
|
|
31
|
+
from statistics import NormalDist
|
|
32
|
+
|
|
33
|
+
Z = NormalDist()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fail(msg):
|
|
37
|
+
sys.stderr.write("error: " + msg + "\n")
|
|
38
|
+
sys.exit(2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_rate(text, name, allow_pp=False):
|
|
42
|
+
s = str(text).strip().lower().replace(" ", "")
|
|
43
|
+
try:
|
|
44
|
+
if s.endswith("pp"):
|
|
45
|
+
if not allow_pp:
|
|
46
|
+
fail(name + ": 'pp' only applies to --detect-abs")
|
|
47
|
+
return float(s[:-2]) / 100.0
|
|
48
|
+
if s.endswith("%"):
|
|
49
|
+
return float(s[:-1]) / 100.0
|
|
50
|
+
v = float(s)
|
|
51
|
+
except ValueError:
|
|
52
|
+
fail(name + ": not a number: " + str(text))
|
|
53
|
+
return v
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fmt_pct(x, digits=2):
|
|
57
|
+
return ("{:." + str(digits) + "f}").format(x * 100).rstrip("0").rstrip(".") + "%"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fmt_pp(x):
|
|
61
|
+
return "{:.3f}".format(x * 100).rstrip("0").rstrip(".") + " pp"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fmt_hours(h):
|
|
65
|
+
if h is None:
|
|
66
|
+
return "n/a"
|
|
67
|
+
if h < 1:
|
|
68
|
+
return "{:.0f} min".format(max(1, math.ceil(h * 60)))
|
|
69
|
+
if h < 48:
|
|
70
|
+
return "{:.1f} h".format(h)
|
|
71
|
+
return "{:.1f} h ({:.1f} d)".format(h, h / 24)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def z_values(alpha, power, one_sided):
|
|
75
|
+
za = Z.inv_cdf(1 - alpha) if one_sided else Z.inv_cdf(1 - alpha / 2)
|
|
76
|
+
zb = Z.inv_cdf(power)
|
|
77
|
+
return za, zb
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def equal_arm_n(p1, p2, za, zb):
|
|
81
|
+
"""Units per arm for two equal arms."""
|
|
82
|
+
pbar = (p1 + p2) / 2
|
|
83
|
+
num = za * math.sqrt(2 * pbar * (1 - pbar)) + zb * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))
|
|
84
|
+
return math.ceil((num / (p2 - p1)) ** 2)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def hours_needed(p1, p2, e, c, traffic, za, zb):
|
|
88
|
+
"""Hours until a treatment arm of share e and control of share c is powered.
|
|
89
|
+
|
|
90
|
+
n1 = traffic*e*t (treatment, rate p2), n2 = traffic*c*t (control, rate p1).
|
|
91
|
+
Both arms scale with t, so the power equation solves in closed form.
|
|
92
|
+
"""
|
|
93
|
+
if e <= 0 or c <= 0:
|
|
94
|
+
return None
|
|
95
|
+
pbar = (e * p2 + c * p1) / (e + c)
|
|
96
|
+
a = za * math.sqrt(pbar * (1 - pbar) * (1 / e + 1 / c))
|
|
97
|
+
b = zb * math.sqrt(p2 * (1 - p2) / e + p1 * (1 - p1) / c)
|
|
98
|
+
return ((a + b) / abs(p2 - p1)) ** 2 / traffic
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def detectable_in(p1, direction, e, c, traffic, za, zb, hours):
|
|
102
|
+
"""Smallest absolute regression detectable in `hours` (bisection)."""
|
|
103
|
+
if e <= 0 or c <= 0:
|
|
104
|
+
return None
|
|
105
|
+
hi = (1 - p1) if direction == "up" else p1
|
|
106
|
+
hi *= 0.999
|
|
107
|
+
lo = 1e-9
|
|
108
|
+
if hours_needed(p1, p1 + hi if direction == "up" else p1 - hi, e, c, traffic, za, zb) > hours:
|
|
109
|
+
return None
|
|
110
|
+
for _ in range(80):
|
|
111
|
+
mid = (lo + hi) / 2
|
|
112
|
+
p2 = p1 + mid if direction == "up" else p1 - mid
|
|
113
|
+
if hours_needed(p1, p2, e, c, traffic, za, zb) <= hours:
|
|
114
|
+
hi = mid
|
|
115
|
+
else:
|
|
116
|
+
lo = mid
|
|
117
|
+
return hi
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def smallest_sufficient_exposure(p1, p2, traffic, za, zb, max_hours, control_pct):
|
|
121
|
+
"""Smallest exposure (0.1% steps, up to 50%) that is powered within max_hours."""
|
|
122
|
+
for tenth in range(1, 501):
|
|
123
|
+
e = tenth / 1000.0
|
|
124
|
+
c = control_pct if control_pct is not None else 1 - e
|
|
125
|
+
if e + c > 1 + 1e-9:
|
|
126
|
+
break
|
|
127
|
+
h = hours_needed(p1, p2, e, c, traffic, za, zb)
|
|
128
|
+
if h is not None and h <= max_hours:
|
|
129
|
+
return e, h
|
|
130
|
+
return None, None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
ap = argparse.ArgumentParser(description="Hours each canary/flag exposure step must run to detect a regression.")
|
|
135
|
+
ap.add_argument("--baseline", required=True, help="baseline rate of the metric in control, e.g. 0.8%% or 0.008")
|
|
136
|
+
g = ap.add_mutually_exclusive_group(required=True)
|
|
137
|
+
g.add_argument("--detect-abs", help="smallest regression worth catching, absolute: 0.3pp or 0.003")
|
|
138
|
+
g.add_argument("--detect-rel", help="smallest regression worth catching, relative to baseline: 25%% or 0.25")
|
|
139
|
+
ap.add_argument("--traffic", required=True, type=float, help="units per hour across all users (requests or users)")
|
|
140
|
+
ap.add_argument("--unit", default="requests", help="what --traffic counts; use the unit the flag randomises on (default: requests)")
|
|
141
|
+
ap.add_argument("--steps", default="1,5,25,50,100", help="exposure percentages, comma-separated (default: 1,5,25,50,100)")
|
|
142
|
+
ap.add_argument("--control-pct", help="fixed control (holdback) size in percent; default: everyone not exposed")
|
|
143
|
+
ap.add_argument("--direction", choices=["up", "down"], default="up",
|
|
144
|
+
help="up = a rise is the regression (error rate, default); down = a fall is (conversion)")
|
|
145
|
+
ap.add_argument("--alpha", type=float, default=0.05, help="false-alarm rate (default 0.05)")
|
|
146
|
+
ap.add_argument("--power", type=float, default=0.8, help="chance of catching a real regression (default 0.8)")
|
|
147
|
+
ap.add_argument("--one-sided", action="store_true", help="one-sided test (only the regression direction); default two-sided")
|
|
148
|
+
ap.add_argument("--max-hours", type=float, default=72.0, help="longest sensible step (default 72)")
|
|
149
|
+
ap.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
|
|
150
|
+
args = ap.parse_args()
|
|
151
|
+
|
|
152
|
+
p1 = parse_rate(args.baseline, "--baseline")
|
|
153
|
+
if not 0 < p1 < 1:
|
|
154
|
+
fail("--baseline must be between 0 and 100%")
|
|
155
|
+
if args.detect_abs is not None:
|
|
156
|
+
d = parse_rate(args.detect_abs, "--detect-abs", allow_pp=True)
|
|
157
|
+
else:
|
|
158
|
+
d = parse_rate(args.detect_rel, "--detect-rel") * p1
|
|
159
|
+
if d <= 0:
|
|
160
|
+
fail("the regression to detect must be greater than zero")
|
|
161
|
+
p2 = p1 + d if args.direction == "up" else p1 - d
|
|
162
|
+
if not 0 < p2 < 1:
|
|
163
|
+
fail("baseline plus the regression falls outside 0-100%")
|
|
164
|
+
if args.traffic <= 0:
|
|
165
|
+
fail("--traffic must be greater than zero")
|
|
166
|
+
if not 0 < args.alpha < 1 or not 0 < args.power < 1:
|
|
167
|
+
fail("--alpha and --power must be between 0 and 1")
|
|
168
|
+
if args.max_hours <= 0:
|
|
169
|
+
fail("--max-hours must be greater than zero")
|
|
170
|
+
control = None
|
|
171
|
+
if args.control_pct is not None:
|
|
172
|
+
control = parse_rate(args.control_pct.rstrip("%") + "%", "--control-pct")
|
|
173
|
+
if not 0 < control < 1:
|
|
174
|
+
fail("--control-pct must be between 0 and 100")
|
|
175
|
+
try:
|
|
176
|
+
steps = [float(s.strip().rstrip("%")) / 100.0 for s in args.steps.split(",") if s.strip()]
|
|
177
|
+
except ValueError:
|
|
178
|
+
fail("--steps must be comma-separated percentages, e.g. 1,5,25,50,100")
|
|
179
|
+
if not steps or any(not 0 < s <= 1 for s in steps):
|
|
180
|
+
fail("--steps must be percentages between 0 and 100")
|
|
181
|
+
|
|
182
|
+
za, zb = z_values(args.alpha, args.power, args.one_sided)
|
|
183
|
+
n_equal = equal_arm_n(p1, p2, za, zb)
|
|
184
|
+
|
|
185
|
+
rows = []
|
|
186
|
+
for e in steps:
|
|
187
|
+
c = control if control is not None else 1 - e
|
|
188
|
+
row = {"exposure_pct": round(e * 100, 4), "control_pct": round(c * 100, 4)}
|
|
189
|
+
if e + c > 1 + 1e-9:
|
|
190
|
+
row.update(status="invalid", note="exposure plus control exceeds 100%")
|
|
191
|
+
elif c <= 1e-9:
|
|
192
|
+
row.update(status="no-control", hours=None,
|
|
193
|
+
note="no concurrent control at 100%: promote on the previous step's comparison, "
|
|
194
|
+
"or keep a holdback (--control-pct) to keep comparing")
|
|
195
|
+
else:
|
|
196
|
+
h = hours_needed(p1, p2, e, c, args.traffic, za, zb)
|
|
197
|
+
row["hours"] = round(h, 3)
|
|
198
|
+
row["treatment_units"] = math.ceil(h * args.traffic * e)
|
|
199
|
+
row["control_units"] = math.ceil(h * args.traffic * c)
|
|
200
|
+
if h <= args.max_hours:
|
|
201
|
+
row["status"] = "sufficient"
|
|
202
|
+
else:
|
|
203
|
+
row["status"] = "insufficient"
|
|
204
|
+
mde = detectable_in(p1, args.direction, e, c, args.traffic, za, zb, args.max_hours)
|
|
205
|
+
row["detectable_in_max_hours_abs"] = None if mde is None else round(mde, 6)
|
|
206
|
+
rows.append(row)
|
|
207
|
+
|
|
208
|
+
best_e, best_h = smallest_sufficient_exposure(p1, p2, args.traffic, za, zb, args.max_hours, control)
|
|
209
|
+
|
|
210
|
+
result = {
|
|
211
|
+
"baseline": p1, "regression_abs": round(d, 6), "regressed_rate": round(p2, 6),
|
|
212
|
+
"direction": args.direction, "alpha": args.alpha, "power": args.power,
|
|
213
|
+
"test": "one-sided" if args.one_sided else "two-sided",
|
|
214
|
+
"traffic_per_hour": args.traffic, "unit": args.unit, "max_hours": args.max_hours,
|
|
215
|
+
"equal_arm_units_per_arm": n_equal, "steps": rows,
|
|
216
|
+
"smallest_sufficient_exposure_pct": None if best_e is None else round(best_e * 100, 1),
|
|
217
|
+
"smallest_sufficient_exposure_hours": None if best_h is None else round(best_h, 2),
|
|
218
|
+
}
|
|
219
|
+
if args.json:
|
|
220
|
+
print(json.dumps(result, indent=2))
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
u = args.unit
|
|
224
|
+
print("# Canary sufficiency")
|
|
225
|
+
print()
|
|
226
|
+
print("- Metric: baseline {} -> regression to catch {} ({} rate {}), {} test, alpha {}, power {}".format(
|
|
227
|
+
fmt_pct(p1, 3), fmt_pp(d), "rising to" if args.direction == "up" else "falling to",
|
|
228
|
+
fmt_pct(p2, 3), result["test"], args.alpha, args.power))
|
|
229
|
+
print("- Traffic: {:,.0f} {} per hour; control = {}".format(
|
|
230
|
+
args.traffic, u, "fixed {} holdback".format(fmt_pct(control)) if control is not None else "everyone not exposed"))
|
|
231
|
+
print("- Equal arms would need {:,} {} per arm".format(n_equal, u))
|
|
232
|
+
print()
|
|
233
|
+
print("| Exposure | Control | Treatment {u} needed | Control {u} needed | Minimum time | Status |".format(u=u))
|
|
234
|
+
print("|---|---|---|---|---|---|")
|
|
235
|
+
for r in rows:
|
|
236
|
+
exp = fmt_pct(r["exposure_pct"] / 100)
|
|
237
|
+
ctl = fmt_pct(r["control_pct"] / 100)
|
|
238
|
+
if r["status"] in ("no-control", "invalid"):
|
|
239
|
+
print("| {} | {} | - | - | n/a | {} |".format(exp, ctl, r["note"]))
|
|
240
|
+
continue
|
|
241
|
+
status = "ok" if r["status"] == "sufficient" else "INSUFFICIENT: over {:g} h".format(args.max_hours)
|
|
242
|
+
if r["status"] == "insufficient":
|
|
243
|
+
m = r.get("detectable_in_max_hours_abs")
|
|
244
|
+
status += "; in {:g} h it can only catch {}".format(
|
|
245
|
+
args.max_hours, "nothing measurable" if m is None else ">= " + fmt_pp(m))
|
|
246
|
+
print("| {} | {} | {:,} | {:,} | {} | {} |".format(
|
|
247
|
+
exp, ctl, r["treatment_units"], r["control_units"], fmt_hours(r["hours"]), status))
|
|
248
|
+
print()
|
|
249
|
+
if best_e is None:
|
|
250
|
+
print("No exposure up to 50% reaches sufficiency within {:g} h. Catch a larger regression, "
|
|
251
|
+
"run longer, or pick a metric with a higher baseline rate.".format(args.max_hours))
|
|
252
|
+
else:
|
|
253
|
+
print("Smallest exposure that reaches sufficiency within {:g} h: {} (about {}).".format(
|
|
254
|
+
args.max_hours, fmt_pct(best_e, 1), fmt_hours(best_h)))
|
|
255
|
+
print()
|
|
256
|
+
print("Minimum time is statistics only. A step also runs through at least one daily peak "
|
|
257
|
+
"and any delayed effect (nightly jobs, next-day emails) before it is promoted.")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Rollout plan — {change name}
|
|
2
|
+
|
|
3
|
+
**{Two lines: the answer to the user's question, the input fact that settles it, and the shape of the plan.}**
|
|
4
|
+
✅ *"No: at 2,000 users an hour a 1% step needs about 20 days to see a 0.3 pp error rise, so Tuesday-to-Friday proves nothing. Run internal for a day, 10% for 3 days, 50% for a day, and reach 100% the Wednesday after."*
|
|
5
|
+
|
|
6
|
+
**Binding constraint:** {the fact that paces the plan: a partner cap, a billing or batch cycle, a deadline, the segments the flag can target, a shadow run, or volume. "None: traffic is high, the default ladder applies."}
|
|
7
|
+
**Flag:** `{key}`, evaluated {server / client / edge}, targets by {user / account / tenant / %} · **Assumptions:** {one line, each tagged `[assumption]`}
|
|
8
|
+
|
|
9
|
+
## Ladder
|
|
10
|
+
|
|
11
|
+
| Step | Dates | Who is in it, and why | Judged on | Promote when |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| 0 | {Mon 7 Sep – Tue 8 Sep} | {internal / test accounts: crashes at no customer cost} | crashes only | {no abort for 24 h} |
|
|
14
|
+
| 1 | {…} | {named cohort or `hash(salt + id) mod 100 < 10`; why these; who is held back} | {units or cycle this step sees, in one phrase} | {every threshold below holds at the step's end} |
|
|
15
|
+
| 2 | {…} | | | |
|
|
16
|
+
| {n} | {date} | everyone | {holdback or the last comparison} | — |
|
|
17
|
+
|
|
18
|
+
{One sentence of arithmetic, with its inputs: what the smallest useful step is and why.}
|
|
19
|
+
{Deadline line when there is one: ✅ *"100% on 14 Sep, 9 days before the deadline; one failed cycle misses it."*}
|
|
20
|
+
|
|
21
|
+
## Thresholds
|
|
22
|
+
|
|
23
|
+
All compare the flag-on cohort with the flag-off cohort over the same window. Counts, not
|
|
24
|
+
percentages, where a step sees fewer than about 100 of the unit.
|
|
25
|
+
|
|
26
|
+
| Metric | Baseline | Promote if | Abort (any time, {automatic / manual}) |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| {decision metric} | {from input} | {≤ control + 0.3 pp / zero unexplained} | {> 3× control for 5 min → flag off} |
|
|
29
|
+
| {guardrail} | | | |
|
|
30
|
+
|
|
31
|
+
- **Hold:** {sample short or metric between threshold and abort → extend once by {time or cycle}, then roll back}.
|
|
32
|
+
- **Watcher:** {role} · **Sign-off before each promotion:** {role, if one is needed} · **Override:** {one role, reason written down}.
|
|
33
|
+
|
|
34
|
+
## What turning the flag off does not undo
|
|
35
|
+
|
|
36
|
+
Flag off: {who}, takes effect in {seconds / cache TTL / next app launch}. Only rows this change touches:
|
|
37
|
+
|
|
38
|
+
| What | Undone? | Handling |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| {messages sent / money moved / records written in a new format / third-party records} | no | {correction or reissue path, reversal procedure, what keeps reading the new format} |
|
|
41
|
+
|
|
42
|
+
**Point of no return:** {the step and why / none}.
|
|
43
|
+
|
|
44
|
+
## Flag removal
|
|
45
|
+
|
|
46
|
+
**Owner:** {name from the input, else role} · **Date:** {date} · **Gate:** {100% for 14 days or one full cycle, no rollback} · **Removes:** `{key}`, the old path, its config and dashboards · **Keeps:** {any reader the old records need}.
|
|
47
|
+
|
|
48
|
+
## To confirm
|
|
49
|
+
|
|
50
|
+
- {at most 4 facts, each one that would change a step}
|
|
@@ -33,5 +33,17 @@
|
|
|
33
33
|
"model": {
|
|
34
34
|
"tier": "standard",
|
|
35
35
|
"rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalates on very large diffs."
|
|
36
|
+
},
|
|
37
|
+
"permissions": {
|
|
38
|
+
"network": [
|
|
39
|
+
"sonarcloud.io"
|
|
40
|
+
],
|
|
41
|
+
"commands": [
|
|
42
|
+
"sonar-scanner",
|
|
43
|
+
"git",
|
|
44
|
+
"curl"
|
|
45
|
+
],
|
|
46
|
+
"files": "none",
|
|
47
|
+
"notes": "No scripts. Its instructions run sonar-scanner and query the Sonar API with curl, using SONAR_TOKEN from the environment, against sonarcloud.io or the self-hosted SonarQube URL the user configures."
|
|
36
48
|
}
|
|
37
49
|
}
|
|
@@ -37,5 +37,16 @@
|
|
|
37
37
|
"model": {
|
|
38
38
|
"tier": "standard",
|
|
39
39
|
"rationale": "Structured generation against fixed module conventions and a secure-defaults checklist; the judgment is bounded to choosing defaults and validation rules for a described need."
|
|
40
|
+
},
|
|
41
|
+
"permissions": {
|
|
42
|
+
"network": [
|
|
43
|
+
"registry.terraform.io",
|
|
44
|
+
"releases.hashicorp.com"
|
|
45
|
+
],
|
|
46
|
+
"commands": [
|
|
47
|
+
"terraform"
|
|
48
|
+
],
|
|
49
|
+
"files": "none",
|
|
50
|
+
"notes": "No scripts. Its instructions run `terraform fmt`, `terraform init -backend=false` and `terraform validate` on the module it writes. `init` downloads the providers from the Terraform registry; none of the commands contacts a cloud account."
|
|
40
51
|
}
|
|
41
52
|
}
|
|
@@ -40,7 +40,14 @@
|
|
|
40
40
|
"iam",
|
|
41
41
|
"drift"
|
|
42
42
|
],
|
|
43
|
-
"permissions": {
|
|
43
|
+
"permissions": {
|
|
44
|
+
"network": [],
|
|
45
|
+
"commands": [
|
|
46
|
+
"terraform"
|
|
47
|
+
],
|
|
48
|
+
"files": "named-paths",
|
|
49
|
+
"notes": "Reads a plan JSON, which can hold secrets in clear text; the script never prints attribute values. Its instructions run `terraform show -json` to produce that plan JSON."
|
|
50
|
+
},
|
|
44
51
|
"model": {
|
|
45
52
|
"tier": "heavy",
|
|
46
53
|
"rationale": "Adversarial review of a destructive change: judging which replacements lose data, which access changes widen exposure, and what is out of scope needs careful reasoning; a missed destroy costs far more than the model."
|
|
@@ -28,7 +28,18 @@
|
|
|
28
28
|
"design",
|
|
29
29
|
"api"
|
|
30
30
|
],
|
|
31
|
-
"permissions": {
|
|
31
|
+
"permissions": {
|
|
32
|
+
"network": [
|
|
33
|
+
"api.figma.com",
|
|
34
|
+
"pypi.org",
|
|
35
|
+
"files.pythonhosted.org"
|
|
36
|
+
],
|
|
37
|
+
"commands": [
|
|
38
|
+
"pip"
|
|
39
|
+
],
|
|
40
|
+
"files": "named-paths",
|
|
41
|
+
"notes": "Reads FIGMA_TOKEN from the environment to call the Figma REST API. Its instructions install the pinned requirements once with `python3 -m pip install -r requirements.txt`, which reaches PyPI."
|
|
42
|
+
},
|
|
32
43
|
"model": {
|
|
33
44
|
"tier": "standard",
|
|
34
45
|
"rationale": "API calls + spec generation. Bounded transformation."
|