skilldrop-cli 1.3.0 → 1.5.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 +10 -10
- package/bin/skilldrop.js +15 -10
- package/catalogue.json +8 -4
- package/contracts/catalogue.schema.json +2 -1
- package/model-routing.json +70 -78
- 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 +29 -13
- package/packs/ai-engineering/skills/judge-audit/evals/evals.json +30 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/12/judge_runs.csv +241 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/12/rosa_labels.csv +33 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/13/moderation_eval.csv +601 -0
- package/packs/ai-engineering/skills/judge-audit/manifest.json +2 -3
- 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/ai-engineering/skills/mcp-pairing/manifest.json +1 -1
- package/packs/ai-engineering/skills/mcp-pairing/reference.md +4 -3
- package/packs/ai-engineering/skills/subagent-design/SKILL.md +1 -0
- package/packs/ai-engineering/skills/subagent-design/examples/help-center-localization.md +146 -0
- package/packs/ai-engineering/skills/subagent-design/manifest.json +2 -2
- package/packs/api-builder/skills/prompt-caching-advisor/SKILL.md +4 -0
- package/packs/api-builder/skills/prompt-caching-advisor/examples/support-bot-block-order.md +120 -0
- package/packs/api-builder/skills/prompt-caching-advisor/manifest.json +2 -2
- package/packs/api-builder/skills/tool-use-schema-writer/manifest.json +2 -2
- 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/LOOP.md +4 -4
- 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/loops/release/loop.json +1 -1
- package/packs/dev-team/skills/bug-triage/SKILL.md +1 -0
- package/packs/dev-team/skills/bug-triage/examples/export-row-limit-escalation.md +126 -0
- package/packs/dev-team/skills/bug-triage/manifest.json +2 -2
- 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/skills/prd-draft/SKILL.md +1 -0
- package/packs/product-manager/skills/prd-draft/examples/approver-delay-not-slack.md +142 -0
- package/packs/product-manager/skills/prd-draft/manifest.json +2 -2
- 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/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/profiles.json +1 -1
- package/packs/ai-engineering/loops/adopt/LOOP.md +0 -120
- package/packs/ai-engineering/loops/adopt/evals/eval_queries.json +0 -10
- package/packs/ai-engineering/loops/adopt/evals/evals.json +0 -69
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/ai-ideas.md +0 -15
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/org-context.md +0 -27
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/staff-survey.csv +0 -11
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/ai-ideas.md +0 -13
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/firm-context.md +0 -23
- package/packs/ai-engineering/loops/adopt/loop.json +0 -53
- 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/career/loops/apply/LOOP.md +0 -97
- package/packs/career/loops/apply/evals/eval_queries.json +0 -9
- package/packs/career/loops/apply/evals/evals.json +0 -27
- package/packs/career/loops/apply/loop.json +0 -52
- package/packs/product-manager/loops/discover/LOOP.md +0 -82
- package/packs/product-manager/loops/discover/evals/eval_queries.json +0 -30
- package/packs/product-manager/loops/discover/evals/evals.json +0 -26
- package/packs/product-manager/loops/discover/loop.json +0 -41
- 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
- package/packs/sre-oncall/loops/operate/LOOP.md +0 -84
- package/packs/sre-oncall/loops/operate/evals/eval_queries.json +0 -30
- package/packs/sre-oncall/loops/operate/evals/evals.json +0 -25
- package/packs/sre-oncall/loops/operate/loop.json +0 -41
package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/schema.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Schema notes: Ovenbird (bakery scheduling software), PostgreSQL 15, schema `app`
|
|
2
|
+
|
|
3
|
+
A **customer** is a business account (one bakery or bakery chain). A customer has one or
|
|
4
|
+
more **users** (staff seats). Billing is per customer, monthly or annual.
|
|
5
|
+
|
|
6
|
+
All timestamps are `timestamptz`, stored in UTC.
|
|
7
|
+
|
|
8
|
+
## app.customers
|
|
9
|
+
| column | type | notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| customer_id | bigint PK | |
|
|
12
|
+
| name | text | |
|
|
13
|
+
| status | text | `trial`, `active`, `cancelled` |
|
|
14
|
+
| billing_interval | text | `monthly` or `annual` |
|
|
15
|
+
| created_at | timestamptz | |
|
|
16
|
+
| cancelled_at | timestamptz | NULL unless cancelled |
|
|
17
|
+
|
|
18
|
+
## app.users
|
|
19
|
+
One row per staff seat. A customer has between 1 and 60 users; the median is 3.
|
|
20
|
+
| column | type | notes |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| user_id | bigint PK | |
|
|
23
|
+
| customer_id | bigint FK | |
|
|
24
|
+
| email | text | |
|
|
25
|
+
| last_login_at | timestamptz | overwritten on every login; only the most recent login is kept |
|
|
26
|
+
|
|
27
|
+
## app.login_events
|
|
28
|
+
One row per login. Retained for 13 months.
|
|
29
|
+
| column | type | notes |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| login_id | bigint PK | |
|
|
32
|
+
| user_id | bigint FK | |
|
|
33
|
+
| logged_in_at | timestamptz | |
|
|
34
|
+
|
|
35
|
+
## app.payments
|
|
36
|
+
One row per payment attempt.
|
|
37
|
+
| column | type | notes |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| payment_id | bigint PK | |
|
|
40
|
+
| customer_id | bigint FK | |
|
|
41
|
+
| amount_pence | integer | |
|
|
42
|
+
| status | text | `succeeded`, `failed`, `refunded` |
|
|
43
|
+
| paid_at | timestamptz | |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Active customers, October 2026 (Finance)
|
|
2
|
+
-- A customer is active if they made a successful payment in the month.
|
|
3
|
+
SELECT COUNT(DISTINCT p.customer_id) AS active_customers
|
|
4
|
+
FROM app.payments p
|
|
5
|
+
WHERE p.status = 'succeeded'
|
|
6
|
+
AND p.paid_at >= '2026-10-01'
|
|
7
|
+
AND p.paid_at < '2026-11-01';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Active customers, October 2026 (Growth)
|
|
2
|
+
-- A customer is active if someone on the account logged in during the month.
|
|
3
|
+
-- Trials excluded.
|
|
4
|
+
SELECT COUNT(*) AS active_customers
|
|
5
|
+
FROM app.customers c
|
|
6
|
+
JOIN app.users u ON u.customer_id = c.customer_id
|
|
7
|
+
WHERE c.status <> 'trial'
|
|
8
|
+
AND u.last_login_at >= '2026-10-01'
|
|
9
|
+
AND u.last_login_at < '2026-10-31';
|
package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/thread.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# #exec-dashboard (exported thread, 3 November 2026)
|
|
2
|
+
|
|
3
|
+
**Rosa Lindgren (Chief of Staff), 09:12**
|
|
4
|
+
Reminder that the exec dashboard build starts Monday. The top row is one tile: Active
|
|
5
|
+
customers, October. I need one number. Right now I have two.
|
|
6
|
+
|
|
7
|
+
**Bilal Haddad (Finance), 09:20**
|
|
8
|
+
Finance number is 41,200. That's customers who actually paid us in October. It's what goes
|
|
9
|
+
in the board pack and what investors see. Query: finance_active_customers.sql
|
|
10
|
+
|
|
11
|
+
**Jo Ferreira (Growth), 09:24**
|
|
12
|
+
Growth number is 58,900. Plenty of customers are on annual plans and didn't pay in
|
|
13
|
+
October, but they're very much active, they're using the product every day. Counting only
|
|
14
|
+
payers undercounts us badly. Query: growth_active_customers.sql
|
|
15
|
+
|
|
16
|
+
**Bilal Haddad (Finance), 09:31**
|
|
17
|
+
"Using the product" isn't a customer, Jo. Trial accounts log in too.
|
|
18
|
+
|
|
19
|
+
**Jo Ferreira (Growth), 09:33**
|
|
20
|
+
Trials are excluded in mine.
|
|
21
|
+
|
|
22
|
+
**Rosa Lindgren (Chief of Staff), 09:40**
|
|
23
|
+
I don't mind which, I just need it to be right and to not change every month. Can someone
|
|
24
|
+
sort this out before Monday?
|
|
25
|
+
|
|
26
|
+
**Bilal Haddad (Finance), 09:41**
|
|
27
|
+
Happy for analytics to call it.
|
|
28
|
+
|
|
29
|
+
**Jo Ferreira (Growth), 09:42**
|
|
30
|
+
Same.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
From: Anouk Verhoeven (Head of Retention, Larkspur Kitchens)
|
|
2
|
+
To: Analytics
|
|
3
|
+
Subject: Retention dashboard for the Monday win-back meeting
|
|
4
|
+
|
|
5
|
+
Hi all,
|
|
6
|
+
|
|
7
|
+
Every Monday the retention team meets to decide which plan cohorts get a win-back offer
|
|
8
|
+
that week (we have budget for about two offers a week, so we have to choose). Right now we
|
|
9
|
+
do it off three spreadsheets that never agree. We want one dashboard instead.
|
|
10
|
+
|
|
11
|
+
What we need to answer in that meeting, most important first:
|
|
12
|
+
|
|
13
|
+
1. Which plans are losing subscribers fastest right now? (churn rate by plan, weekly)
|
|
14
|
+
2. Is the active subscriber base growing or shrinking, by plan?
|
|
15
|
+
3. How much net revenue is each plan bringing in per week, so we know what a lost
|
|
16
|
+
subscriber is worth?
|
|
17
|
+
4. Did last week's win-back offer bring people back? (we log offers in `winback_offers`)
|
|
18
|
+
|
|
19
|
+
Plans: Classic, Veggie, Family, Lean. Our week runs Monday to Sunday, UK time.
|
|
20
|
+
|
|
21
|
+
Metric owners:
|
|
22
|
+
- Churn rate and active subscribers: me (Anouk)
|
|
23
|
+
- Net revenue: Callum Reyes in Finance. He's particular that refunds come off and test
|
|
24
|
+
accounts are excluded.
|
|
25
|
+
|
|
26
|
+
The BI team (Sana's team) will build it in our usual tool once the spec is agreed. I've put
|
|
27
|
+
the queries we use today in the sql/ folder. They're from three different people, so please
|
|
28
|
+
check them before anything gets built on them. Schema notes are in schema.md.
|
|
29
|
+
|
|
30
|
+
The marketing team also asked for page views and app downloads on it. I don't think we use
|
|
31
|
+
those in the meeting.
|
|
32
|
+
|
|
33
|
+
Thanks,
|
|
34
|
+
Anouk
|
|
@@ -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
|
+
```
|