skilldrop-cli 1.3.0 → 1.4.0
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 -4
- package/catalogue.json +4 -4
- package/model-routing.json +62 -70
- package/package.json +1 -1
- package/packs/ai-engineering/skills/agent-adoption-stage/SKILL.md +0 -1
- package/packs/ai-engineering/skills/agent-adoption-stage/manifest.json +3 -4
- package/packs/ai-engineering/skills/ai-adoption-rollout/SKILL.md +0 -1
- package/packs/ai-engineering/skills/ai-adoption-rollout/manifest.json +3 -4
- package/packs/ai-engineering/skills/ai-readiness-assessment/SKILL.md +0 -1
- package/packs/ai-engineering/skills/ai-readiness-assessment/manifest.json +1 -2
- package/packs/ai-engineering/skills/ai-use-case-triage/manifest.json +3 -3
- package/packs/ai-engineering/skills/judge-audit/SKILL.md +1 -2
- package/packs/ai-engineering/skills/judge-audit/manifest.json +1 -2
- package/packs/ai-engineering/skills/llm-eval-harness/SKILL.md +1 -2
- package/packs/ai-engineering/skills/llm-eval-harness/manifest.json +3 -3
- package/packs/api-builder/skills/tool-use-schema-writer/manifest.json +2 -2
- package/packs/career/loops/apply/evals/evals.json +28 -0
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/application-form.md +10 -0
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/job-posting.md +33 -0
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/resume.md +34 -0
- package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/cv.md +27 -0
- package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/warehouse-supervisor.md +30 -0
- package/packs/career/skills/application-form-answers/manifest.json +3 -3
- package/packs/career/skills/cover-letter/manifest.json +3 -3
- package/packs/career/skills/resume-tailor/manifest.json +3 -3
- package/packs/converters/skills/md-to-docx/manifest.json +3 -3
- package/packs/core/loops/ship-a-draft/evals/evals.json +28 -0
- package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-adr-from-thread/slack-thread.txt +47 -0
- package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-announcement-unsettled/ticket-PRD-412.md +44 -0
- package/packs/core/skills/output-hygiene/manifest.json +3 -3
- package/packs/data-analytics/loops/measure/evals/evals.json +30 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/schema.md +43 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/finance_active_customers.sql +7 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/growth_active_customers.sql +9 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/thread.md +30 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/request.md +34 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/schema.md +58 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/active_subscribers.sql +17 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/net_revenue_by_plan.sql +15 -0
- package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/weekly_churn_rate.sql +34 -0
- package/packs/data-analytics/skills/dashboard-spec/SKILL.md +0 -1
- package/packs/data-analytics/skills/dashboard-spec/manifest.json +1 -2
- package/packs/design/skills/slide-outliner/manifest.json +2 -2
- package/packs/dev-team/loops/build/evals/evals.json +28 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/.github/workflows/ci.yml.fixture +19 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/__init__.py +4 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/pricing.py +47 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/pyproject.toml.fixture +9 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/story.md +61 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/__init__.py +0 -0
- package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/test_pricing.py +49 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/.github/workflows/ci.yml.fixture +19 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/bug-report.md +25 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pr-description.md +42 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pyproject.toml.fixture +9 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/__init__.py +4 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/bucket.py +49 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/__init__.py +0 -0
- package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/test_bucket.py +63 -0
- package/packs/dev-team/loops/modernize/evals/evals.json +30 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/.github/workflows/ci.yml.fixture +11 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/README.md +17 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/docs/adr/0004-late-fee-calculation.md +25 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/legacy/feecalc.py +74 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_2026-10.txt +10 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_edge.txt +11 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/tests/test_feecalc_smoke.py +21 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/.github/workflows/ci.yml.fixture +11 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/PR-412.md +24 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/README.md +17 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/docs/adr/0004-late-fee-calculation.md +25 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/__init__.py +1 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/cli.py +33 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/extract.py +33 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/rules.py +39 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/legacy/feecalc.py +74 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_2026-10.txt +10 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_edge.txt +11 -0
- package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/tests/test_feecalc_smoke.py +21 -0
- package/packs/dev-team/loops/release/evals/eval_queries.json +10 -0
- package/packs/dev-team/loops/release/evals/evals.json +34 -0
- package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/0061_switch_to_haldrin_tokens.sql +22 -0
- package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/eng-notes.md +40 -0
- package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/merged-prs.md +13 -0
- package/packs/dev-team/loops/release/evals/files/release-saved-searches/merged-prs.md +13 -0
- package/packs/dev-team/loops/release/evals/files/release-saved-searches/readiness-evidence.md +27 -0
- package/packs/dev-team/loops/release/evals/files/release-saved-searches/traffic-baseline.md +23 -0
- package/packs/dev-team/skills/feature-implement-loop/manifest.json +2 -2
- package/packs/dev-team/skills/pr-description-writer/manifest.json +3 -3
- package/packs/dev-team/skills/pre-merge-review/manifest.json +2 -2
- package/packs/dev-team/skills/release-notes/manifest.json +2 -2
- package/packs/dev-team/skills/test-plan-generator/manifest.json +2 -2
- package/packs/experience-design/skills/service-blueprint/manifest.json +3 -3
- package/packs/experience-design/skills/ux-writing/manifest.json +3 -3
- package/packs/grc/loops/comply/evals/evals.json +29 -0
- package/packs/grc/loops/comply/evals/files/comply-chat-vendor-launch/vendor-brief.md +72 -0
- package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/launch-note.md +31 -0
- package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/risk-register.csv +6 -0
- package/packs/infra-as-code/skills/terraform-module/manifest.json +3 -3
- package/packs/product-manager/loops/discover/evals/evals.json +28 -0
- package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/interview-notes.md +63 -0
- package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/support-tickets.csv +13 -0
- package/packs/product-manager/loops/discover/evals/files/discover-vp-loyalty-no-evidence/vp-message.txt +13 -0
- package/packs/product-manager/skills/user-journey-map/manifest.json +2 -2
- package/packs/research/loops/research/evals/evals.json +28 -0
- package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t1-ticket-volume.md +15 -0
- package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t2-release-notes-4-2.md +11 -0
- package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t3-billing-migration.md +12 -0
- package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t4-agent-poll.md +11 -0
- package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t5-app-store-reviews.md +12 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s1-analytics-export.md +29 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s2-email-delivery-report.md +25 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s3-pricing-changelog.md +9 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s4-trial-interview-notes.md +21 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s5-competitor-note.md +9 -0
- package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s6-weekly-growth-recap.md +7 -0
- package/packs/research/skills/research-plan/manifest.json +3 -3
- package/packs/research/skills/source-synthesis/manifest.json +3 -3
- package/packs/skill-engineering/skills/skill-author/manifest.json +3 -3
- package/packs/solution-architect/loops/design/evals/evals.json +28 -0
- package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0001-postgres-system-of-record.md +23 -0
- package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0002-background-work-via-job-queue.md +23 -0
- package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0003-tenant-isolation-row-level.md +25 -0
- package/packs/solution-architect/loops/design/evals/files/design-bulk-export/prd-bulk-export.md +50 -0
- package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0002-background-work-via-job-queue.md +23 -0
- package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0004-new-stateful-infra-needs-platform-signoff.md +22 -0
- package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/prd-partner-webhooks.md +36 -0
- package/packs/solution-architect/skills/api-contract-draft/manifest.json +3 -3
- package/packs/solution-architect/skills/architecture-diagrams/manifest.json +3 -3
- package/packs/solution-architect/skills/design-doc/manifest.json +2 -2
- package/packs/solution-architect/skills/figma-diagrams/manifest.json +3 -3
- package/packs/sre-oncall/loops/operate/evals/evals.json +28 -0
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/alerts.log +22 -0
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/incident-chat.txt +27 -0
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/service-notes.md +46 -0
- package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/oncall-log.txt +17 -0
- package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/runbook-webhooks.md +38 -0
- package/packs/sre-oncall/skills/cloud-cost-review/SKILL.md +0 -1
- package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -2
- package/packs/sre-oncall/skills/delivery-metrics-report/SKILL.md +0 -1
- package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -2
- package/packs/sre-oncall/skills/incident-comms/manifest.json +2 -2
- package/packs/sre-oncall/skills/postmortem-generator/manifest.json +2 -2
- package/packs/stakeholder-comms/skills/decision-log/manifest.json +2 -2
- package/packs/trackers/skills/team-status-report/manifest.json +3 -3
- package/packs/ai-engineering/skills/ai-usage-report/SKILL.md +0 -111
- package/packs/ai-engineering/skills/ai-usage-report/evals/eval_queries.json +0 -30
- package/packs/ai-engineering/skills/ai-usage-report/evals/evals.json +0 -46
- package/packs/ai-engineering/skills/ai-usage-report/evals/files/1/northwind-ai-events.csv +0 -4213
- package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/draftwell-events.csv +0 -249
- package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/export-note.md +0 -9
- package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/published-assets.csv +0 -44
- package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/team-roster.md +0 -10
- package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +0 -133
- package/packs/ai-engineering/skills/ai-usage-report/manifest.json +0 -12
- package/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +0 -453
- package/packs/ai-engineering/skills/ai-usage-report/templates/report-per-user.md +0 -42
- package/packs/ai-engineering/skills/ai-usage-report/templates/report-team-rollup.md +0 -49
- package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +0 -76
- package/packs/product-manager/skills/lean-canvas/SKILL.md +0 -162
- package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +0 -42
- package/packs/product-manager/skills/lean-canvas/evals/evals.json +0 -82
- package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +0 -54
- package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +0 -30
- package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +0 -44
- package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +0 -5
- package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +0 -40
- package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +0 -18
- package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +0 -72
- package/packs/product-manager/skills/lean-canvas/manifest.json +0 -34
- package/packs/product-manager/skills/lean-canvas/reference.md +0 -102
- package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +0 -211
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Warehouse schema notes (PostgreSQL 15, schema `subs`)
|
|
2
|
+
|
|
3
|
+
All timestamps are `timestamptz` stored in UTC. The business runs on Europe/London time.
|
|
4
|
+
|
|
5
|
+
## subs.subscribers
|
|
6
|
+
One row per subscriber account.
|
|
7
|
+
| column | type | notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| subscriber_id | bigint PK | |
|
|
10
|
+
| email | text | |
|
|
11
|
+
| plan | text | Classic, Veggie, Family, Lean. Current plan only |
|
|
12
|
+
| created_at | timestamptz | sign-up time |
|
|
13
|
+
| cancelled_at | timestamptz | NULL while active; set when the subscription is cancelled |
|
|
14
|
+
|
|
15
|
+
## subs.orders
|
|
16
|
+
One row per weekly box delivered.
|
|
17
|
+
| column | type | notes |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| order_id | bigint PK | |
|
|
20
|
+
| subscriber_id | bigint FK | |
|
|
21
|
+
| plan | text | plan at time of order |
|
|
22
|
+
| order_total_pence | integer | total charged for the box, incl. add-ons |
|
|
23
|
+
| ordered_at | timestamptz | |
|
|
24
|
+
|
|
25
|
+
## subs.order_items
|
|
26
|
+
One row per line item in a box (recipes and add-ons). A box usually has 3 to 6 items.
|
|
27
|
+
| column | type | notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| order_item_id | bigint PK | |
|
|
30
|
+
| order_id | bigint FK | |
|
|
31
|
+
| sku | text | |
|
|
32
|
+
| item_price_pence | integer | |
|
|
33
|
+
|
|
34
|
+
## subs.refunds
|
|
35
|
+
Zero or more rows per order (partial refunds are common, e.g. one missing ingredient).
|
|
36
|
+
| column | type | notes |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| refund_id | bigint PK | |
|
|
39
|
+
| order_id | bigint FK | |
|
|
40
|
+
| refund_pence | integer | |
|
|
41
|
+
| refunded_at | timestamptz | |
|
|
42
|
+
|
|
43
|
+
## subs.test_accounts
|
|
44
|
+
Accounts used by QA and the warehouse team. Some rows are placeholder accounts created
|
|
45
|
+
before the subscriber was provisioned, so `subscriber_id` can be NULL.
|
|
46
|
+
| column | type | notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| email | text | |
|
|
49
|
+
| subscriber_id | bigint | nullable |
|
|
50
|
+
|
|
51
|
+
## subs.winback_offers
|
|
52
|
+
| column | type | notes |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| offer_id | bigint PK | |
|
|
55
|
+
| subscriber_id | bigint FK | |
|
|
56
|
+
| sent_at | timestamptz | |
|
|
57
|
+
| offer_code | text | |
|
|
58
|
+
| redeemed_at | timestamptz | NULL if not redeemed |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Active subscribers by plan at the end of each week
|
|
2
|
+
-- author: growth-analytics
|
|
3
|
+
SELECT
|
|
4
|
+
date_trunc('week', d.day) AS week_start,
|
|
5
|
+
s.plan,
|
|
6
|
+
COUNT(DISTINCT s.subscriber_id) AS active_subscribers
|
|
7
|
+
FROM generate_series(
|
|
8
|
+
(now() - interval '12 weeks')::date,
|
|
9
|
+
now()::date,
|
|
10
|
+
interval '1 day'
|
|
11
|
+
) AS d(day)
|
|
12
|
+
JOIN subs.subscribers s
|
|
13
|
+
ON s.created_at <= d.day
|
|
14
|
+
AND (s.cancelled_at IS NULL OR s.cancelled_at > d.day)
|
|
15
|
+
WHERE extract(isodow FROM d.day) = 7 -- Sunday = end of week
|
|
16
|
+
GROUP BY 1, 2
|
|
17
|
+
ORDER BY 1, 2;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Net revenue per plan per week (Callum's definition: gross minus refunds, no test accounts)
|
|
2
|
+
-- author: finance-analytics
|
|
3
|
+
SELECT
|
|
4
|
+
date_trunc('week', o.ordered_at) AS week_start,
|
|
5
|
+
o.plan,
|
|
6
|
+
SUM(o.order_total_pence) / 100.0 AS gross_revenue_gbp,
|
|
7
|
+
SUM(COALESCE(r.refund_pence, 0)) / 100.0 AS refunds_gbp,
|
|
8
|
+
(SUM(o.order_total_pence) - SUM(COALESCE(r.refund_pence, 0))) / 100.0 AS net_revenue_gbp,
|
|
9
|
+
COUNT(DISTINCT oi.sku) AS distinct_skus
|
|
10
|
+
FROM subs.orders o
|
|
11
|
+
JOIN subs.order_items oi ON oi.order_id = o.order_id
|
|
12
|
+
LEFT JOIN subs.refunds r ON r.order_id = o.order_id
|
|
13
|
+
WHERE o.subscriber_id NOT IN (SELECT subscriber_id FROM subs.test_accounts)
|
|
14
|
+
GROUP BY 1, 2
|
|
15
|
+
ORDER BY 1, 2;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Weekly churn rate by plan = cancellations in week / active at start of week
|
|
2
|
+
-- author: retention-team
|
|
3
|
+
WITH weeks AS (
|
|
4
|
+
SELECT generate_series(
|
|
5
|
+
date_trunc('week', now()) - interval '12 weeks',
|
|
6
|
+
date_trunc('week', now()),
|
|
7
|
+
interval '1 week'
|
|
8
|
+
) AS week_start
|
|
9
|
+
),
|
|
10
|
+
active_at_start AS (
|
|
11
|
+
SELECT w.week_start, s.plan, COUNT(*) AS active_start
|
|
12
|
+
FROM weeks w
|
|
13
|
+
JOIN subs.subscribers s
|
|
14
|
+
ON s.created_at < w.week_start
|
|
15
|
+
AND (s.cancelled_at IS NULL OR s.cancelled_at >= w.week_start)
|
|
16
|
+
GROUP BY 1, 2
|
|
17
|
+
),
|
|
18
|
+
cancels AS (
|
|
19
|
+
SELECT date_trunc('week', s.cancelled_at) AS week_start, s.plan, COUNT(*) AS cancelled
|
|
20
|
+
FROM subs.subscribers s
|
|
21
|
+
WHERE s.cancelled_at IS NOT NULL
|
|
22
|
+
GROUP BY 1, 2
|
|
23
|
+
)
|
|
24
|
+
SELECT
|
|
25
|
+
a.week_start,
|
|
26
|
+
a.plan,
|
|
27
|
+
a.active_start,
|
|
28
|
+
COALESCE(c.cancelled, 0) AS cancelled,
|
|
29
|
+
COALESCE(c.cancelled, 0) / a.active_start * 100 AS churn_rate_pct
|
|
30
|
+
FROM active_at_start a
|
|
31
|
+
LEFT JOIN cancels c
|
|
32
|
+
ON c.week_start = a.week_start
|
|
33
|
+
AND c.plan = a.plan
|
|
34
|
+
ORDER BY 1, 2;
|
|
@@ -69,7 +69,6 @@ Writes the one-page brief a BI developer builds from, so the dashboard answers t
|
|
|
69
69
|
- ❌ Defining how one metric is calculated (formula, filters, timezone) — that's the metric owner's call; this skill flags the gap, it doesn't fill it
|
|
70
70
|
- ❌ Choosing which metric judges a feature, with targets and a decision rule — that's `success-metrics`
|
|
71
71
|
- ❌ Service health dashboards, SLOs and alerting — that's `observability-plan`
|
|
72
|
-
- ❌ A report on AI tool usage from telemetry logs — that's `ai-usage-report`
|
|
73
72
|
|
|
74
73
|
## Anti-patterns to avoid
|
|
75
74
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dashboard-spec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Spec a dashboard before anyone builds it — the audience and the decision it serves, the questions in priority order, one chart per question with the chart type and why, the defined metric behind each chart, filters, refresh cadence and freshness SLA, and a list of what is deliberately left off, with vanity metrics cut. Use when the user says \"spec a dashboard\", \"what should go on our KPI dashboard\", \"the BI team needs requirements for a dashboard\", \"our dashboard has 40 charts and nobody uses it\", or \"design the exec dashboard\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"optional": []
|
|
13
13
|
},
|
|
14
14
|
"related": [
|
|
15
|
-
"ai-usage-report",
|
|
16
15
|
"observability-plan",
|
|
17
16
|
"success-metrics"
|
|
18
17
|
],
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slide-outliner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Outline an architecture review or technical pitch deck — slide titles, key points per slide, and speaker notes. Does NOT generate PowerPoint/Keynote files; produces a structured outline the user can paste into any deck tool. Use when the user is preparing for an architecture review, design review, sales pitch, partner meeting, or steering-committee presentation.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
7
7
|
"env": { "required": [], "optional": [] },
|
|
8
8
|
"related": [],
|
|
9
9
|
"tags": ["presentation", "deck", "outline", "stakeholder-communication"],
|
|
10
|
-
"model": { "tier": "
|
|
10
|
+
"model": { "tier": "light", "rationale": "Measured (RFC-0073): on its held-out evals the light tier met 86% and 90% of checks in two tier trials, against standard's 83% and heavy's 83% and 93%, within 5 points of the best both times." }
|
|
11
11
|
}
|
|
@@ -22,6 +22,34 @@
|
|
|
22
22
|
"Treats RECONSIDER or SPLIT as leaving the loop, not as a revise",
|
|
23
23
|
"Leaves the merge decision itself to a human at G2.1"
|
|
24
24
|
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "build-promo-codes",
|
|
28
|
+
"held_out": "2026-11-04",
|
|
29
|
+
"prompt": "Can you implement STORY-142 (story.md) in this repo and tell me if it's ready to merge? The package is cartcalc/, tests in tests/.",
|
|
30
|
+
"assertions": [
|
|
31
|
+
"Gets oriented before writing code: takes the test command from the CI workflow (python -m unittest discover -s tests -t .) and notes the module's conventions (Decimal money, never float, round to cents once at the end), and the new code follows them",
|
|
32
|
+
"Each of the six acceptance criteria is covered by at least one test, including AC6's 10.85 total, which only comes out if the discounted subtotal is not rounded before tax",
|
|
33
|
+
"Existing callers keep working: cart_total still accepts the old arguments and the eight original tests pass without being edited",
|
|
34
|
+
"Runs the mechanical gate after implementing, reports the actual command and its result, and gives a READY or NOT READY verdict that follows from that run rather than from a reading of the code",
|
|
35
|
+
"Keeps the generate-and-review cycle to at most three rounds, and any gap the review finds is either closed in code or listed as open",
|
|
36
|
+
"Stops at the human merge gate: does not merge, push, or say the change is merged, and leaves the merge decision to the user",
|
|
37
|
+
"Does not invent requirements that are not in the story, such as stacking codes, fixed-amount codes, per-code usage limits or storing codes"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "build-rate-limiter-not-ready",
|
|
42
|
+
"held_out": "2026-11-04",
|
|
43
|
+
"prompt": "I fixed BUG-311 (bug-report.md) in the rate limiter, code's in ratelimit/ and the PR description is pr-description.md. Is it safe to merge? I need it in before the 5pm freeze.",
|
|
44
|
+
"assertions": [
|
|
45
|
+
"Runs the test suite the way CI does and reports that test_burst_allows_exactly_capacity fails (4 != 5), instead of relying on the PR's 'All tests pass locally'",
|
|
46
|
+
"Traces the failure to the change from >= to > in allow(), the PR's 'small tidy-up', and explains that it throttles every tenant one request below its burst capacity, a regression unrelated to BUG-311",
|
|
47
|
+
"Confirms separately that the min() cap in _refill() does fix the BUG-311 repro (an idle bucket then 50 calls lets through at most 5)",
|
|
48
|
+
"The mechanical gate decides: the verdict for the branch as submitted is NOT READY, and neither the 5pm freeze nor a favourable code read overrides the failing test",
|
|
49
|
+
"Does not move on to a merge recommendation or a ship decision while the mechanical gate is failing; the merge stays the user's call",
|
|
50
|
+
"If it proposes or applies a fix, it does not weaken, skip or delete the failing test, and it re-runs the suite and reports the new result rather than assuming it passes",
|
|
51
|
+
"Does not invent facts that are not in the input, such as CI run results, other affected tenants, or production traffic figures"
|
|
52
|
+
]
|
|
25
53
|
}
|
|
26
54
|
]
|
|
27
55
|
}
|
package/packs/dev-team/loops/build/evals/files/build-promo-codes/.github/workflows/ci.yml.fixture
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
13
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Byte-compile
|
|
17
|
+
run: python -m compileall -q cartcalc tests
|
|
18
|
+
- name: Unit tests
|
|
19
|
+
run: python -m unittest discover -s tests -t . -v
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Cart totals.
|
|
2
|
+
|
|
3
|
+
Conventions in this module:
|
|
4
|
+
- All money is Decimal. Never float.
|
|
5
|
+
- Intermediate values are kept at full precision; round to cents once, at the end,
|
|
6
|
+
half-up (finance signed this off in 2025, see docs/rounding.md in the main repo).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from decimal import ROUND_HALF_UP, Decimal
|
|
12
|
+
from typing import Iterable
|
|
13
|
+
|
|
14
|
+
CENT = Decimal("0.01")
|
|
15
|
+
ZERO = Decimal("0")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class LineItem:
|
|
20
|
+
sku: str
|
|
21
|
+
unit_price: Decimal
|
|
22
|
+
quantity: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_cents(amount: Decimal) -> Decimal:
|
|
26
|
+
"""Round a money amount to cents, half-up."""
|
|
27
|
+
return amount.quantize(CENT, rounding=ROUND_HALF_UP)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def subtotal(items: Iterable[LineItem]) -> Decimal:
|
|
31
|
+
"""Sum of unit_price * quantity, unrounded."""
|
|
32
|
+
total = ZERO
|
|
33
|
+
for item in items:
|
|
34
|
+
if item.quantity < 0:
|
|
35
|
+
raise ValueError(f"negative quantity for {item.sku}")
|
|
36
|
+
if item.unit_price < ZERO:
|
|
37
|
+
raise ValueError(f"negative price for {item.sku}")
|
|
38
|
+
total += item.unit_price * item.quantity
|
|
39
|
+
return total
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cart_total(items: Iterable[LineItem], tax_rate: Decimal = ZERO) -> Decimal:
|
|
43
|
+
"""Total to charge: subtotal plus tax, rounded to cents once."""
|
|
44
|
+
if tax_rate < ZERO:
|
|
45
|
+
raise ValueError("tax_rate must not be negative")
|
|
46
|
+
sub = subtotal(items)
|
|
47
|
+
return to_cents(sub + sub * tax_rate)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# STORY-142: Percentage promo codes at checkout
|
|
2
|
+
|
|
3
|
+
**As a** shopper
|
|
4
|
+
**I want** to enter a promo code at checkout
|
|
5
|
+
**So that** I get the advertised percentage off my order
|
|
6
|
+
|
|
7
|
+
Epic: SPRING-PROMOS. Estimate: 3. Agreed at refinement on 29 Oct 2026 (Lena, Marco, Yusuf).
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Marketing will hand engineering a list of codes for the spring campaign. For now the
|
|
12
|
+
codes are passed in to `cart_total` as a catalogue (a dict of code -> PromoCode); storing
|
|
13
|
+
them is a separate story (STORY-150). Existing callers of `cart_total` (the basket page
|
|
14
|
+
and the order-confirmation email) must keep working unchanged.
|
|
15
|
+
|
|
16
|
+
A `PromoCode` has: `code` (str), `percent_off` (Decimal, e.g. `Decimal("10")` for 10%),
|
|
17
|
+
`max_discount` (Decimal or None), `expires_on` (date or None, last valid day inclusive).
|
|
18
|
+
|
|
19
|
+
## Acceptance criteria
|
|
20
|
+
|
|
21
|
+
```gherkin
|
|
22
|
+
Scenario: AC1 percentage discount
|
|
23
|
+
Given the catalogue has SAVE10 at 10% off with no cap and no expiry
|
|
24
|
+
And the cart subtotal is 50.00
|
|
25
|
+
When the shopper applies "SAVE10" with no tax
|
|
26
|
+
Then the total is 45.00
|
|
27
|
+
|
|
28
|
+
Scenario: AC2 code matching is forgiving
|
|
29
|
+
Given the catalogue has SAVE10
|
|
30
|
+
When the shopper applies " save10 "
|
|
31
|
+
Then SAVE10 is applied
|
|
32
|
+
|
|
33
|
+
Scenario: AC3 discount cap
|
|
34
|
+
Given the catalogue has BIG20 at 20% off with a max discount of 50.00
|
|
35
|
+
And the cart subtotal is 400.00
|
|
36
|
+
When the shopper applies "BIG20" with no tax
|
|
37
|
+
Then the total is 350.00
|
|
38
|
+
|
|
39
|
+
Scenario: AC4 expired code
|
|
40
|
+
Given SPRING15 expires on 2026-03-31
|
|
41
|
+
When the shopper applies "SPRING15" on 2026-04-01
|
|
42
|
+
Then a PromoError is raised whose reason is "expired"
|
|
43
|
+
And SPRING15 still applies on 2026-03-31
|
|
44
|
+
|
|
45
|
+
Scenario: AC5 unknown code
|
|
46
|
+
When the shopper applies "NOPE"
|
|
47
|
+
Then a PromoError is raised whose reason is "unknown"
|
|
48
|
+
|
|
49
|
+
Scenario: AC6 tax applies after the discount, rounding once
|
|
50
|
+
Given the catalogue has SAVE10
|
|
51
|
+
And the cart subtotal is 10.05
|
|
52
|
+
When the shopper applies "SAVE10" with a tax rate of 0.2
|
|
53
|
+
Then the total is 10.85
|
|
54
|
+
# not 10.86: the discounted subtotal (9.045) is not rounded before tax
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Out of scope
|
|
58
|
+
|
|
59
|
+
- More than one code per order.
|
|
60
|
+
- Fixed-amount codes (e.g. 5.00 off).
|
|
61
|
+
- Storing codes, or usage limits per code (STORY-150).
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from cartcalc import LineItem, cart_total, subtotal, to_cents
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def D(s):
|
|
8
|
+
return Decimal(s)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubtotalTests(unittest.TestCase):
|
|
12
|
+
def test_sums_lines(self):
|
|
13
|
+
items = [LineItem("A", D("2.50"), 2), LineItem("B", D("10.00"), 1)]
|
|
14
|
+
self.assertEqual(subtotal(items), D("15.00"))
|
|
15
|
+
|
|
16
|
+
def test_empty_cart_is_zero(self):
|
|
17
|
+
self.assertEqual(subtotal([]), D("0"))
|
|
18
|
+
|
|
19
|
+
def test_negative_quantity_rejected(self):
|
|
20
|
+
with self.assertRaises(ValueError):
|
|
21
|
+
subtotal([LineItem("A", D("1.00"), -1)])
|
|
22
|
+
|
|
23
|
+
def test_negative_price_rejected(self):
|
|
24
|
+
with self.assertRaises(ValueError):
|
|
25
|
+
subtotal([LineItem("A", D("-1.00"), 1)])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CartTotalTests(unittest.TestCase):
|
|
29
|
+
def test_no_tax(self):
|
|
30
|
+
self.assertEqual(cart_total([LineItem("A", D("19.99"), 1)]), D("19.99"))
|
|
31
|
+
|
|
32
|
+
def test_tax_rounded_once_half_up(self):
|
|
33
|
+
# 3 x 0.335 = 1.005; with 10% tax = 1.1055 -> 1.11
|
|
34
|
+
items = [LineItem("A", D("0.335"), 3)]
|
|
35
|
+
self.assertEqual(cart_total(items, tax_rate=D("0.1")), D("1.11"))
|
|
36
|
+
|
|
37
|
+
def test_negative_tax_rejected(self):
|
|
38
|
+
with self.assertRaises(ValueError):
|
|
39
|
+
cart_total([LineItem("A", D("1.00"), 1)], tax_rate=D("-0.1"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ToCentsTests(unittest.TestCase):
|
|
43
|
+
def test_half_up(self):
|
|
44
|
+
self.assertEqual(to_cents(D("2.345")), D("2.35"))
|
|
45
|
+
self.assertEqual(to_cents(D("2.344")), D("2.34"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
13
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Byte-compile
|
|
17
|
+
run: python -m compileall -q ratelimit tests
|
|
18
|
+
- name: Unit tests
|
|
19
|
+
run: python -m unittest discover -s tests -t . -v
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# BUG-311: Idle tenants can burst far past their plan limit
|
|
2
|
+
|
|
3
|
+
- Reported: 2026-10-30 by Support (Nadia Haddad), from ticket SUP-9081
|
|
4
|
+
- Component: `ratelimit` (API gateway token bucket)
|
|
5
|
+
- Severity: S2 (customers can exceed their plan; no data exposure). Priority: P1.
|
|
6
|
+
|
|
7
|
+
## Symptom
|
|
8
|
+
|
|
9
|
+
Tenant acct-7781 is on the Starter plan: burst capacity 5, refill 5 tokens per second.
|
|
10
|
+
After their integration had been idle overnight, gateway logs show 37 requests from
|
|
11
|
+
them accepted within a single second at 07:02:11 UTC on 2026-10-30.
|
|
12
|
+
|
|
13
|
+
## Repro (verified by Ahmed on a laptop, 2026-10-31)
|
|
14
|
+
|
|
15
|
+
1. Create `TokenBucket(capacity=5, refill_per_sec=5, clock=fake_clock)`.
|
|
16
|
+
2. Advance the fake clock by 3600 seconds without calling `allow()`.
|
|
17
|
+
3. Call `allow()` 50 times without advancing the clock.
|
|
18
|
+
|
|
19
|
+
Expected: 5 calls return True, the rest False.
|
|
20
|
+
Actual: 50 calls return True.
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
Refill adds `elapsed * refill_per_sec` with no ceiling, so the bucket grows without
|
|
25
|
+
limit while idle.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# PR #487: Fix BUG-311 — cap token bucket refill at capacity
|
|
2
|
+
|
|
3
|
+
Branch: `fix/bug-311-bucket-cap` Author: Jonah Becker
|
|
4
|
+
|
|
5
|
+
## What
|
|
6
|
+
|
|
7
|
+
- `_refill()` now caps tokens at `capacity`, so an idle bucket can't grow past its burst size.
|
|
8
|
+
- Added `test_idle_bucket_does_not_exceed_capacity` (the BUG-311 repro).
|
|
9
|
+
- Small tidy-up in `allow()` while I was in there.
|
|
10
|
+
|
|
11
|
+
## Testing
|
|
12
|
+
|
|
13
|
+
All tests pass locally.
|
|
14
|
+
|
|
15
|
+
## Diff
|
|
16
|
+
|
|
17
|
+
```diff
|
|
18
|
+
--- a/ratelimit/bucket.py
|
|
19
|
+
+++ b/ratelimit/bucket.py
|
|
20
|
+
@@ def _refill(self) -> None:
|
|
21
|
+
now = self._clock()
|
|
22
|
+
elapsed = max(0.0, now - self._last)
|
|
23
|
+
- self._tokens = self._tokens + elapsed * self.refill_per_sec
|
|
24
|
+
+ self._tokens = min(self.capacity, self._tokens + elapsed * self.refill_per_sec)
|
|
25
|
+
self._last = now
|
|
26
|
+
@@ def allow(self, cost: float = 1.0) -> bool:
|
|
27
|
+
self._refill()
|
|
28
|
+
- if self._tokens >= cost:
|
|
29
|
+
+ if self._tokens > cost:
|
|
30
|
+
self._tokens -= cost
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
--- a/tests/test_bucket.py
|
|
34
|
+
+++ b/tests/test_bucket.py
|
|
35
|
+
@@ class TokenBucketTests(unittest.TestCase):
|
|
36
|
+
+ def test_idle_bucket_does_not_exceed_capacity(self):
|
|
37
|
+
+ # BUG-311 repro
|
|
38
|
+
+ bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
39
|
+
+ self.clock.advance(3600)
|
|
40
|
+
+ self.assertLessEqual(bucket.tokens, 5)
|
|
41
|
+
+ self.assertLessEqual(burst(bucket, 50), 5)
|
|
42
|
+
```
|
package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/bucket.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Token bucket rate limiter.
|
|
2
|
+
|
|
3
|
+
A bucket holds up to `capacity` tokens and gains `refill_per_sec` tokens per second.
|
|
4
|
+
Each request spends `cost` tokens; a request is allowed when enough tokens are left.
|
|
5
|
+
The clock is injectable so tests do not sleep.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenBucket:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
capacity: float,
|
|
17
|
+
refill_per_sec: float,
|
|
18
|
+
clock: Callable[[], float] = time.monotonic,
|
|
19
|
+
) -> None:
|
|
20
|
+
if capacity <= 0:
|
|
21
|
+
raise ValueError("capacity must be positive")
|
|
22
|
+
if refill_per_sec < 0:
|
|
23
|
+
raise ValueError("refill_per_sec must not be negative")
|
|
24
|
+
self.capacity = float(capacity)
|
|
25
|
+
self.refill_per_sec = float(refill_per_sec)
|
|
26
|
+
self._clock = clock
|
|
27
|
+
self._tokens = float(capacity)
|
|
28
|
+
self._last = clock()
|
|
29
|
+
|
|
30
|
+
def _refill(self) -> None:
|
|
31
|
+
now = self._clock()
|
|
32
|
+
elapsed = max(0.0, now - self._last)
|
|
33
|
+
self._tokens = min(self.capacity, self._tokens + elapsed * self.refill_per_sec)
|
|
34
|
+
self._last = now
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def tokens(self) -> float:
|
|
38
|
+
self._refill()
|
|
39
|
+
return self._tokens
|
|
40
|
+
|
|
41
|
+
def allow(self, cost: float = 1.0) -> bool:
|
|
42
|
+
"""Spend `cost` tokens if available. Returns True if the request is allowed."""
|
|
43
|
+
if cost <= 0:
|
|
44
|
+
raise ValueError("cost must be positive")
|
|
45
|
+
self._refill()
|
|
46
|
+
if self._tokens > cost:
|
|
47
|
+
self._tokens -= cost
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
File without changes
|
package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/test_bucket.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from ratelimit import TokenBucket
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeClock:
|
|
7
|
+
def __init__(self, start: float = 1000.0) -> None:
|
|
8
|
+
self.now = start
|
|
9
|
+
|
|
10
|
+
def __call__(self) -> float:
|
|
11
|
+
return self.now
|
|
12
|
+
|
|
13
|
+
def advance(self, seconds: float) -> None:
|
|
14
|
+
self.now += seconds
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def burst(bucket: TokenBucket, n: int) -> int:
|
|
18
|
+
return sum(1 for _ in range(n) if bucket.allow())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokenBucketTests(unittest.TestCase):
|
|
22
|
+
def setUp(self) -> None:
|
|
23
|
+
self.clock = FakeClock()
|
|
24
|
+
|
|
25
|
+
def test_burst_allows_exactly_capacity(self):
|
|
26
|
+
bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
27
|
+
self.assertEqual(burst(bucket, 10), 5)
|
|
28
|
+
|
|
29
|
+
def test_refills_over_time(self):
|
|
30
|
+
bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
31
|
+
burst(bucket, 10)
|
|
32
|
+
self.clock.advance(0.5)
|
|
33
|
+
self.assertGreaterEqual(bucket.tokens, 2.5)
|
|
34
|
+
|
|
35
|
+
def test_idle_bucket_does_not_exceed_capacity(self):
|
|
36
|
+
# BUG-311 repro
|
|
37
|
+
bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
38
|
+
self.clock.advance(3600)
|
|
39
|
+
self.assertLessEqual(bucket.tokens, 5)
|
|
40
|
+
self.assertLessEqual(burst(bucket, 50), 5)
|
|
41
|
+
|
|
42
|
+
def test_clock_going_backwards_does_not_add_tokens(self):
|
|
43
|
+
bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
44
|
+
burst(bucket, 5)
|
|
45
|
+
self.clock.advance(-10)
|
|
46
|
+
self.assertFalse(bucket.allow())
|
|
47
|
+
|
|
48
|
+
def test_cost_larger_than_capacity_is_refused(self):
|
|
49
|
+
bucket = TokenBucket(capacity=5, refill_per_sec=5, clock=self.clock)
|
|
50
|
+
self.assertFalse(bucket.allow(cost=6))
|
|
51
|
+
|
|
52
|
+
def test_rejects_bad_arguments(self):
|
|
53
|
+
with self.assertRaises(ValueError):
|
|
54
|
+
TokenBucket(capacity=0, refill_per_sec=1, clock=self.clock)
|
|
55
|
+
with self.assertRaises(ValueError):
|
|
56
|
+
TokenBucket(capacity=1, refill_per_sec=-1, clock=self.clock)
|
|
57
|
+
bucket = TokenBucket(capacity=1, refill_per_sec=1, clock=self.clock)
|
|
58
|
+
with self.assertRaises(ValueError):
|
|
59
|
+
bucket.allow(cost=0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
unittest.main()
|