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.
Files changed (172) hide show
  1. package/README.md +4 -4
  2. package/catalogue.json +4 -4
  3. package/model-routing.json +62 -70
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/agent-adoption-stage/SKILL.md +0 -1
  6. package/packs/ai-engineering/skills/agent-adoption-stage/manifest.json +3 -4
  7. package/packs/ai-engineering/skills/ai-adoption-rollout/SKILL.md +0 -1
  8. package/packs/ai-engineering/skills/ai-adoption-rollout/manifest.json +3 -4
  9. package/packs/ai-engineering/skills/ai-readiness-assessment/SKILL.md +0 -1
  10. package/packs/ai-engineering/skills/ai-readiness-assessment/manifest.json +1 -2
  11. package/packs/ai-engineering/skills/ai-use-case-triage/manifest.json +3 -3
  12. package/packs/ai-engineering/skills/judge-audit/SKILL.md +1 -2
  13. package/packs/ai-engineering/skills/judge-audit/manifest.json +1 -2
  14. package/packs/ai-engineering/skills/llm-eval-harness/SKILL.md +1 -2
  15. package/packs/ai-engineering/skills/llm-eval-harness/manifest.json +3 -3
  16. package/packs/api-builder/skills/tool-use-schema-writer/manifest.json +2 -2
  17. package/packs/career/loops/apply/evals/evals.json +28 -0
  18. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/application-form.md +10 -0
  19. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/job-posting.md +33 -0
  20. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/resume.md +34 -0
  21. package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/cv.md +27 -0
  22. package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/warehouse-supervisor.md +30 -0
  23. package/packs/career/skills/application-form-answers/manifest.json +3 -3
  24. package/packs/career/skills/cover-letter/manifest.json +3 -3
  25. package/packs/career/skills/resume-tailor/manifest.json +3 -3
  26. package/packs/converters/skills/md-to-docx/manifest.json +3 -3
  27. package/packs/core/loops/ship-a-draft/evals/evals.json +28 -0
  28. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-adr-from-thread/slack-thread.txt +47 -0
  29. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-announcement-unsettled/ticket-PRD-412.md +44 -0
  30. package/packs/core/skills/output-hygiene/manifest.json +3 -3
  31. package/packs/data-analytics/loops/measure/evals/evals.json +30 -0
  32. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/schema.md +43 -0
  33. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/finance_active_customers.sql +7 -0
  34. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/growth_active_customers.sql +9 -0
  35. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/thread.md +30 -0
  36. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/request.md +34 -0
  37. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/schema.md +58 -0
  38. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/active_subscribers.sql +17 -0
  39. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/net_revenue_by_plan.sql +15 -0
  40. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/weekly_churn_rate.sql +34 -0
  41. package/packs/data-analytics/skills/dashboard-spec/SKILL.md +0 -1
  42. package/packs/data-analytics/skills/dashboard-spec/manifest.json +1 -2
  43. package/packs/design/skills/slide-outliner/manifest.json +2 -2
  44. package/packs/dev-team/loops/build/evals/evals.json +28 -0
  45. package/packs/dev-team/loops/build/evals/files/build-promo-codes/.github/workflows/ci.yml.fixture +19 -0
  46. package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/__init__.py +4 -0
  47. package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/pricing.py +47 -0
  48. package/packs/dev-team/loops/build/evals/files/build-promo-codes/pyproject.toml.fixture +9 -0
  49. package/packs/dev-team/loops/build/evals/files/build-promo-codes/story.md +61 -0
  50. package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/__init__.py +0 -0
  51. package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/test_pricing.py +49 -0
  52. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/.github/workflows/ci.yml.fixture +19 -0
  53. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/bug-report.md +25 -0
  54. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pr-description.md +42 -0
  55. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pyproject.toml.fixture +9 -0
  56. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/__init__.py +4 -0
  57. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/bucket.py +49 -0
  58. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/__init__.py +0 -0
  59. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/test_bucket.py +63 -0
  60. package/packs/dev-team/loops/modernize/evals/evals.json +30 -0
  61. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/.github/workflows/ci.yml.fixture +11 -0
  62. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/README.md +17 -0
  63. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/docs/adr/0004-late-fee-calculation.md +25 -0
  64. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/legacy/feecalc.py +74 -0
  65. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_2026-10.txt +10 -0
  66. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_edge.txt +11 -0
  67. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/tests/test_feecalc_smoke.py +21 -0
  68. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/.github/workflows/ci.yml.fixture +11 -0
  69. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/PR-412.md +24 -0
  70. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/README.md +17 -0
  71. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/docs/adr/0004-late-fee-calculation.md +25 -0
  72. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/__init__.py +1 -0
  73. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/cli.py +33 -0
  74. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/extract.py +33 -0
  75. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/rules.py +39 -0
  76. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/legacy/feecalc.py +74 -0
  77. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_2026-10.txt +10 -0
  78. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_edge.txt +11 -0
  79. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/tests/test_feecalc_smoke.py +21 -0
  80. package/packs/dev-team/loops/release/evals/eval_queries.json +10 -0
  81. package/packs/dev-team/loops/release/evals/evals.json +34 -0
  82. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/0061_switch_to_haldrin_tokens.sql +22 -0
  83. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/eng-notes.md +40 -0
  84. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/merged-prs.md +13 -0
  85. package/packs/dev-team/loops/release/evals/files/release-saved-searches/merged-prs.md +13 -0
  86. package/packs/dev-team/loops/release/evals/files/release-saved-searches/readiness-evidence.md +27 -0
  87. package/packs/dev-team/loops/release/evals/files/release-saved-searches/traffic-baseline.md +23 -0
  88. package/packs/dev-team/skills/feature-implement-loop/manifest.json +2 -2
  89. package/packs/dev-team/skills/pr-description-writer/manifest.json +3 -3
  90. package/packs/dev-team/skills/pre-merge-review/manifest.json +2 -2
  91. package/packs/dev-team/skills/release-notes/manifest.json +2 -2
  92. package/packs/dev-team/skills/test-plan-generator/manifest.json +2 -2
  93. package/packs/experience-design/skills/service-blueprint/manifest.json +3 -3
  94. package/packs/experience-design/skills/ux-writing/manifest.json +3 -3
  95. package/packs/grc/loops/comply/evals/evals.json +29 -0
  96. package/packs/grc/loops/comply/evals/files/comply-chat-vendor-launch/vendor-brief.md +72 -0
  97. package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/launch-note.md +31 -0
  98. package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/risk-register.csv +6 -0
  99. package/packs/infra-as-code/skills/terraform-module/manifest.json +3 -3
  100. package/packs/product-manager/loops/discover/evals/evals.json +28 -0
  101. package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/interview-notes.md +63 -0
  102. package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/support-tickets.csv +13 -0
  103. package/packs/product-manager/loops/discover/evals/files/discover-vp-loyalty-no-evidence/vp-message.txt +13 -0
  104. package/packs/product-manager/skills/user-journey-map/manifest.json +2 -2
  105. package/packs/research/loops/research/evals/evals.json +28 -0
  106. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t1-ticket-volume.md +15 -0
  107. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t2-release-notes-4-2.md +11 -0
  108. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t3-billing-migration.md +12 -0
  109. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t4-agent-poll.md +11 -0
  110. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t5-app-store-reviews.md +12 -0
  111. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s1-analytics-export.md +29 -0
  112. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s2-email-delivery-report.md +25 -0
  113. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s3-pricing-changelog.md +9 -0
  114. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s4-trial-interview-notes.md +21 -0
  115. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s5-competitor-note.md +9 -0
  116. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s6-weekly-growth-recap.md +7 -0
  117. package/packs/research/skills/research-plan/manifest.json +3 -3
  118. package/packs/research/skills/source-synthesis/manifest.json +3 -3
  119. package/packs/skill-engineering/skills/skill-author/manifest.json +3 -3
  120. package/packs/solution-architect/loops/design/evals/evals.json +28 -0
  121. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0001-postgres-system-of-record.md +23 -0
  122. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0002-background-work-via-job-queue.md +23 -0
  123. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0003-tenant-isolation-row-level.md +25 -0
  124. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/prd-bulk-export.md +50 -0
  125. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0002-background-work-via-job-queue.md +23 -0
  126. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0004-new-stateful-infra-needs-platform-signoff.md +22 -0
  127. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/prd-partner-webhooks.md +36 -0
  128. package/packs/solution-architect/skills/api-contract-draft/manifest.json +3 -3
  129. package/packs/solution-architect/skills/architecture-diagrams/manifest.json +3 -3
  130. package/packs/solution-architect/skills/design-doc/manifest.json +2 -2
  131. package/packs/solution-architect/skills/figma-diagrams/manifest.json +3 -3
  132. package/packs/sre-oncall/loops/operate/evals/evals.json +28 -0
  133. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/alerts.log +22 -0
  134. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/incident-chat.txt +27 -0
  135. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/service-notes.md +46 -0
  136. package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/oncall-log.txt +17 -0
  137. package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/runbook-webhooks.md +38 -0
  138. package/packs/sre-oncall/skills/cloud-cost-review/SKILL.md +0 -1
  139. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -2
  140. package/packs/sre-oncall/skills/delivery-metrics-report/SKILL.md +0 -1
  141. package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -2
  142. package/packs/sre-oncall/skills/incident-comms/manifest.json +2 -2
  143. package/packs/sre-oncall/skills/postmortem-generator/manifest.json +2 -2
  144. package/packs/stakeholder-comms/skills/decision-log/manifest.json +2 -2
  145. package/packs/trackers/skills/team-status-report/manifest.json +3 -3
  146. package/packs/ai-engineering/skills/ai-usage-report/SKILL.md +0 -111
  147. package/packs/ai-engineering/skills/ai-usage-report/evals/eval_queries.json +0 -30
  148. package/packs/ai-engineering/skills/ai-usage-report/evals/evals.json +0 -46
  149. package/packs/ai-engineering/skills/ai-usage-report/evals/files/1/northwind-ai-events.csv +0 -4213
  150. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/draftwell-events.csv +0 -249
  151. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/export-note.md +0 -9
  152. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/published-assets.csv +0 -44
  153. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/team-roster.md +0 -10
  154. package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +0 -133
  155. package/packs/ai-engineering/skills/ai-usage-report/manifest.json +0 -12
  156. package/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +0 -453
  157. package/packs/ai-engineering/skills/ai-usage-report/templates/report-per-user.md +0 -42
  158. package/packs/ai-engineering/skills/ai-usage-report/templates/report-team-rollup.md +0 -49
  159. package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +0 -76
  160. package/packs/product-manager/skills/lean-canvas/SKILL.md +0 -162
  161. package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +0 -42
  162. package/packs/product-manager/skills/lean-canvas/evals/evals.json +0 -82
  163. package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +0 -54
  164. package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +0 -30
  165. package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +0 -44
  166. package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +0 -5
  167. package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +0 -40
  168. package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +0 -18
  169. package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +0 -72
  170. package/packs/product-manager/skills/lean-canvas/manifest.json +0 -34
  171. package/packs/product-manager/skills/lean-canvas/reference.md +0 -102
  172. 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.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.0",
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": "standard", "rationale": "Outlines a deck to a time budget. Structuring, not deep reasoning." }
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
  }
@@ -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,4 @@
1
+ """cartcalc: basket and checkout totals."""
2
+ from cartcalc.pricing import LineItem, cart_total, subtotal, to_cents
3
+
4
+ __all__ = ["LineItem", "cart_total", "subtotal", "to_cents"]
@@ -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,9 @@
1
+ [project]
2
+ name = "cartcalc"
3
+ version = "0.4.0"
4
+ description = "Basket and checkout totals"
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [tool.setuptools]
9
+ packages = ["cartcalc"]
@@ -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).
@@ -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
+ ```
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "ratelimit"
3
+ version = "2.3.1"
4
+ description = "Per-tenant token buckets for the API gateway"
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [tool.setuptools]
9
+ packages = ["ratelimit"]
@@ -0,0 +1,4 @@
1
+ """ratelimit: per-tenant token buckets for the API gateway."""
2
+ from ratelimit.bucket import TokenBucket
3
+
4
+ __all__ = ["TokenBucket"]
@@ -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
@@ -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()