skilldrop-cli 1.2.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 (208) hide show
  1. package/README.md +5 -5
  2. package/catalogue.json +4 -4
  3. package/model-routing.json +62 -70
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/loops/adopt/LOOP.md +120 -0
  6. package/packs/ai-engineering/loops/adopt/evals/eval_queries.json +10 -0
  7. package/packs/ai-engineering/loops/adopt/evals/evals.json +69 -0
  8. package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/ai-ideas.md +15 -0
  9. package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/org-context.md +27 -0
  10. package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/staff-survey.csv +11 -0
  11. package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/ai-ideas.md +13 -0
  12. package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/firm-context.md +23 -0
  13. package/packs/ai-engineering/loops/adopt/loop.json +53 -0
  14. package/packs/ai-engineering/loops/ship-agent/LOOP.md +128 -0
  15. package/packs/ai-engineering/loops/ship-agent/evals/eval_queries.json +10 -0
  16. package/packs/ai-engineering/loops/ship-agent/evals/evals.json +70 -0
  17. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-auto-reply-exfil-open/agent-brief.md +33 -0
  18. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/agent-brief.md +33 -0
  19. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/claude-settings.json +25 -0
  20. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/sample-merged-prs.md +30 -0
  21. package/packs/ai-engineering/loops/ship-agent/loop.json +53 -0
  22. package/packs/ai-engineering/skills/agent-adoption-stage/SKILL.md +0 -1
  23. package/packs/ai-engineering/skills/agent-adoption-stage/manifest.json +3 -4
  24. package/packs/ai-engineering/skills/ai-adoption-rollout/SKILL.md +0 -1
  25. package/packs/ai-engineering/skills/ai-adoption-rollout/manifest.json +3 -4
  26. package/packs/ai-engineering/skills/ai-readiness-assessment/SKILL.md +0 -1
  27. package/packs/ai-engineering/skills/ai-readiness-assessment/manifest.json +1 -2
  28. package/packs/ai-engineering/skills/ai-use-case-triage/manifest.json +3 -3
  29. package/packs/ai-engineering/skills/judge-audit/SKILL.md +1 -2
  30. package/packs/ai-engineering/skills/judge-audit/manifest.json +1 -2
  31. package/packs/ai-engineering/skills/llm-eval-harness/SKILL.md +1 -2
  32. package/packs/ai-engineering/skills/llm-eval-harness/manifest.json +3 -3
  33. package/packs/api-builder/skills/tool-use-schema-writer/manifest.json +2 -2
  34. package/packs/career/loops/apply/evals/evals.json +28 -0
  35. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/application-form.md +10 -0
  36. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/job-posting.md +33 -0
  37. package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/resume.md +34 -0
  38. package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/cv.md +27 -0
  39. package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/warehouse-supervisor.md +30 -0
  40. package/packs/career/skills/application-form-answers/manifest.json +3 -3
  41. package/packs/career/skills/cover-letter/manifest.json +3 -3
  42. package/packs/career/skills/resume-tailor/manifest.json +3 -3
  43. package/packs/converters/skills/md-to-docx/manifest.json +3 -3
  44. package/packs/core/loops/ship-a-draft/evals/evals.json +28 -0
  45. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-adr-from-thread/slack-thread.txt +47 -0
  46. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-announcement-unsettled/ticket-PRD-412.md +44 -0
  47. package/packs/core/skills/output-hygiene/manifest.json +3 -3
  48. package/packs/data-analytics/loops/measure/evals/evals.json +30 -0
  49. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/schema.md +43 -0
  50. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/finance_active_customers.sql +7 -0
  51. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/sql/growth_active_customers.sql +9 -0
  52. package/packs/data-analytics/loops/measure/evals/files/measure-active-customers-dispute/thread.md +30 -0
  53. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/request.md +34 -0
  54. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/schema.md +58 -0
  55. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/active_subscribers.sql +17 -0
  56. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/net_revenue_by_plan.sql +15 -0
  57. package/packs/data-analytics/loops/measure/evals/files/measure-retention-dashboard/sql/weekly_churn_rate.sql +34 -0
  58. package/packs/data-analytics/skills/dashboard-spec/SKILL.md +0 -1
  59. package/packs/data-analytics/skills/dashboard-spec/manifest.json +1 -2
  60. package/packs/design/skills/slide-outliner/manifest.json +2 -2
  61. package/packs/dev-team/loops/build/evals/evals.json +28 -0
  62. package/packs/dev-team/loops/build/evals/files/build-promo-codes/.github/workflows/ci.yml.fixture +19 -0
  63. package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/__init__.py +4 -0
  64. package/packs/dev-team/loops/build/evals/files/build-promo-codes/cartcalc/pricing.py +47 -0
  65. package/packs/dev-team/loops/build/evals/files/build-promo-codes/pyproject.toml.fixture +9 -0
  66. package/packs/dev-team/loops/build/evals/files/build-promo-codes/story.md +61 -0
  67. package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/__init__.py +0 -0
  68. package/packs/dev-team/loops/build/evals/files/build-promo-codes/tests/test_pricing.py +49 -0
  69. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/.github/workflows/ci.yml.fixture +19 -0
  70. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/bug-report.md +25 -0
  71. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pr-description.md +42 -0
  72. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/pyproject.toml.fixture +9 -0
  73. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/__init__.py +4 -0
  74. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/ratelimit/bucket.py +49 -0
  75. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/__init__.py +0 -0
  76. package/packs/dev-team/loops/build/evals/files/build-rate-limiter-not-ready/tests/test_bucket.py +63 -0
  77. package/packs/dev-team/loops/modernize/evals/evals.json +30 -0
  78. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/.github/workflows/ci.yml.fixture +11 -0
  79. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/README.md +17 -0
  80. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/docs/adr/0004-late-fee-calculation.md +25 -0
  81. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/legacy/feecalc.py +74 -0
  82. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_2026-10.txt +10 -0
  83. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/samples/accounts_edge.txt +11 -0
  84. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-batch-port/tests/test_feecalc_smoke.py +21 -0
  85. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/.github/workflows/ci.yml.fixture +11 -0
  86. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/PR-412.md +24 -0
  87. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/README.md +17 -0
  88. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/docs/adr/0004-late-fee-calculation.md +25 -0
  89. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/__init__.py +1 -0
  90. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/cli.py +33 -0
  91. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/extract.py +33 -0
  92. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/hartwell_fees/rules.py +39 -0
  93. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/legacy/feecalc.py +74 -0
  94. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_2026-10.txt +10 -0
  95. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/samples/accounts_edge.txt +11 -0
  96. package/packs/dev-team/loops/modernize/evals/files/modernize-late-fee-rewrite-not-equivalent/tests/test_feecalc_smoke.py +21 -0
  97. package/packs/dev-team/loops/release/evals/eval_queries.json +10 -0
  98. package/packs/dev-team/loops/release/evals/evals.json +34 -0
  99. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/0061_switch_to_haldrin_tokens.sql +22 -0
  100. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/eng-notes.md +40 -0
  101. package/packs/dev-team/loops/release/evals/files/release-irreversible-tomorrow/merged-prs.md +13 -0
  102. package/packs/dev-team/loops/release/evals/files/release-saved-searches/merged-prs.md +13 -0
  103. package/packs/dev-team/loops/release/evals/files/release-saved-searches/readiness-evidence.md +27 -0
  104. package/packs/dev-team/loops/release/evals/files/release-saved-searches/traffic-baseline.md +23 -0
  105. package/packs/dev-team/skills/feature-implement-loop/manifest.json +2 -2
  106. package/packs/dev-team/skills/pr-description-writer/manifest.json +3 -3
  107. package/packs/dev-team/skills/pre-merge-review/manifest.json +2 -2
  108. package/packs/dev-team/skills/release-notes/manifest.json +2 -2
  109. package/packs/dev-team/skills/test-plan-generator/manifest.json +2 -2
  110. package/packs/experience-design/loops/clarify/LOOP.md +114 -0
  111. package/packs/experience-design/loops/clarify/evals/eval_queries.json +10 -0
  112. package/packs/experience-design/loops/clarify/evals/evals.json +68 -0
  113. package/packs/experience-design/loops/clarify/evals/files/clarify-garden-waste-no-complaint-data/form-fields.csv +12 -0
  114. package/packs/experience-design/loops/clarify/evals/files/clarify-garden-waste-no-complaint-data/garden-waste-page.md +29 -0
  115. package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/backoffice-notes.md +14 -0
  116. package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/calls-q3.csv +8 -0
  117. package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/form-fields.csv +12 -0
  118. package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/permit-page.md +27 -0
  119. package/packs/experience-design/loops/clarify/loop.json +35 -0
  120. package/packs/experience-design/skills/service-blueprint/manifest.json +3 -3
  121. package/packs/experience-design/skills/ux-writing/manifest.json +3 -3
  122. package/packs/grc/loops/comply/evals/evals.json +29 -0
  123. package/packs/grc/loops/comply/evals/files/comply-chat-vendor-launch/vendor-brief.md +72 -0
  124. package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/launch-note.md +31 -0
  125. package/packs/grc/loops/comply/evals/files/comply-loyalty-app-unowned-risks/risk-register.csv +6 -0
  126. package/packs/infra-as-code/loops/provision/LOOP.md +128 -0
  127. package/packs/infra-as-code/loops/provision/evals/eval_queries.json +9 -0
  128. package/packs/infra-as-code/loops/provision/evals/evals.json +67 -0
  129. package/packs/infra-as-code/loops/provision/evals/files/provision-rds-no-plan-yet/.terraform.lock.hcl.fixture +24 -0
  130. package/packs/infra-as-code/loops/provision/evals/files/provision-rds-no-plan-yet/network.md +36 -0
  131. package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/main.tf +74 -0
  132. package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/plan.json +901 -0
  133. package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/variables.tf +47 -0
  134. package/packs/infra-as-code/loops/provision/loop.json +41 -0
  135. package/packs/infra-as-code/skills/terraform-module/manifest.json +3 -3
  136. package/packs/product-manager/loops/discover/evals/evals.json +28 -0
  137. package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/interview-notes.md +63 -0
  138. package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/support-tickets.csv +13 -0
  139. package/packs/product-manager/loops/discover/evals/files/discover-vp-loyalty-no-evidence/vp-message.txt +13 -0
  140. package/packs/product-manager/skills/user-journey-map/manifest.json +2 -2
  141. package/packs/research/loops/research/evals/evals.json +28 -0
  142. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t1-ticket-volume.md +15 -0
  143. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t2-release-notes-4-2.md +11 -0
  144. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t3-billing-migration.md +12 -0
  145. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t4-agent-poll.md +11 -0
  146. package/packs/research/loops/research/evals/files/research-ticket-spike-confounded/sources/t5-app-store-reviews.md +12 -0
  147. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s1-analytics-export.md +29 -0
  148. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s2-email-delivery-report.md +25 -0
  149. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s3-pricing-changelog.md +9 -0
  150. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s4-trial-interview-notes.md +21 -0
  151. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s5-competitor-note.md +9 -0
  152. package/packs/research/loops/research/evals/files/research-trial-conversion-drop/sources/s6-weekly-growth-recap.md +7 -0
  153. package/packs/research/skills/research-plan/manifest.json +3 -3
  154. package/packs/research/skills/source-synthesis/manifest.json +3 -3
  155. package/packs/skill-engineering/skills/skill-author/manifest.json +3 -3
  156. package/packs/solution-architect/loops/design/evals/evals.json +28 -0
  157. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0001-postgres-system-of-record.md +23 -0
  158. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0002-background-work-via-job-queue.md +23 -0
  159. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/adr/0003-tenant-isolation-row-level.md +25 -0
  160. package/packs/solution-architect/loops/design/evals/files/design-bulk-export/prd-bulk-export.md +50 -0
  161. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0002-background-work-via-job-queue.md +23 -0
  162. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/adr/0004-new-stateful-infra-needs-platform-signoff.md +22 -0
  163. package/packs/solution-architect/loops/design/evals/files/design-draft-prd-webhooks/prd-partner-webhooks.md +36 -0
  164. package/packs/solution-architect/skills/api-contract-draft/manifest.json +3 -3
  165. package/packs/solution-architect/skills/architecture-diagrams/manifest.json +3 -3
  166. package/packs/solution-architect/skills/design-doc/manifest.json +2 -2
  167. package/packs/solution-architect/skills/figma-diagrams/manifest.json +3 -3
  168. package/packs/sre-oncall/loops/operate/evals/evals.json +28 -0
  169. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/alerts.log +22 -0
  170. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/incident-chat.txt +27 -0
  171. package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/service-notes.md +46 -0
  172. package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/oncall-log.txt +17 -0
  173. package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/runbook-webhooks.md +38 -0
  174. package/packs/sre-oncall/skills/cloud-cost-review/SKILL.md +0 -1
  175. package/packs/sre-oncall/skills/cloud-cost-review/manifest.json +1 -2
  176. package/packs/sre-oncall/skills/delivery-metrics-report/SKILL.md +0 -1
  177. package/packs/sre-oncall/skills/delivery-metrics-report/manifest.json +1 -2
  178. package/packs/sre-oncall/skills/incident-comms/manifest.json +2 -2
  179. package/packs/sre-oncall/skills/postmortem-generator/manifest.json +2 -2
  180. package/packs/stakeholder-comms/skills/decision-log/manifest.json +2 -2
  181. package/packs/trackers/skills/team-status-report/manifest.json +3 -3
  182. package/packs/ai-engineering/skills/ai-usage-report/SKILL.md +0 -111
  183. package/packs/ai-engineering/skills/ai-usage-report/evals/eval_queries.json +0 -30
  184. package/packs/ai-engineering/skills/ai-usage-report/evals/evals.json +0 -46
  185. package/packs/ai-engineering/skills/ai-usage-report/evals/files/1/northwind-ai-events.csv +0 -4213
  186. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/draftwell-events.csv +0 -249
  187. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/export-note.md +0 -9
  188. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/published-assets.csv +0 -44
  189. package/packs/ai-engineering/skills/ai-usage-report/evals/files/2/team-roster.md +0 -10
  190. package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +0 -133
  191. package/packs/ai-engineering/skills/ai-usage-report/manifest.json +0 -12
  192. package/packs/ai-engineering/skills/ai-usage-report/scripts/build_report.py +0 -453
  193. package/packs/ai-engineering/skills/ai-usage-report/templates/report-per-user.md +0 -42
  194. package/packs/ai-engineering/skills/ai-usage-report/templates/report-team-rollup.md +0 -49
  195. package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +0 -76
  196. package/packs/product-manager/skills/lean-canvas/SKILL.md +0 -162
  197. package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +0 -42
  198. package/packs/product-manager/skills/lean-canvas/evals/evals.json +0 -82
  199. package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +0 -54
  200. package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +0 -30
  201. package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +0 -44
  202. package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +0 -5
  203. package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +0 -40
  204. package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +0 -18
  205. package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +0 -72
  206. package/packs/product-manager/skills/lean-canvas/manifest.json +0 -34
  207. package/packs/product-manager/skills/lean-canvas/reference.md +0 -102
  208. package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +0 -211
@@ -23,6 +23,36 @@
23
23
  "Reviews the changed query again at G9 before returning to sign-off",
24
24
  "Counts the return under the cap of 3"
25
25
  ]
26
+ },
27
+ {
28
+ "id": "measure-retention-dashboard",
29
+ "held_out": "2026-11-04",
30
+ "prompt": "we need a dashboard for the Monday win-back meeting. Anouk's request is in request.md, the queries we use now are in sql/ and the schema's in schema.md. Can you make sure these numbers are right before Sana's team builds it? Postgres.",
31
+ "assertions": [
32
+ "Runs the stages in order: the dashboard spec first, then the SQL review, then a sign-off request to the metric owners. Fixes from the review go back into the metric definitions in the spec",
33
+ "The spec says which decision the dashboard serves (which plan cohorts get this week's win-back offer), gives one chart for each of Anouk's four questions in her order, and leaves page views and app downloads off, saying why",
34
+ "Catches that net_revenue_by_plan.sql inflates revenue: joining order_items repeats each order total once per line item, and the refunds join repeats it again for orders with more than one refund",
35
+ "Catches that NOT IN (SELECT subscriber_id FROM subs.test_accounts) returns no rows because test_accounts.subscriber_id can be NULL. Gives a NOT EXISTS (or equivalent) fix",
36
+ "Catches the integer division in weekly_churn_rate.sql (cancelled / active_start), which rounds churn down to 0 for nearly every week. Gives corrected SQL",
37
+ "Flags that date_trunc('week', ...) on UTC timestamps does not match the Monday-to-Sunday UK-time week Anouk asked for, and gives a fix that sets the time zone to Europe/London",
38
+ "Asks the named owners to sign off each metric definition (Anouk for churn and active subscribers, Callum Reyes for net revenue) and does not record any definition as signed off",
39
+ "Does not invent figures, targets, alert thresholds or metric owners that are not in the request"
40
+ ]
41
+ },
42
+ {
43
+ "id": "measure-active-customers-dispute",
44
+ "held_out": "2026-11-04",
45
+ "prompt": "The exec dashboard gets built next week and it's got one 'Active customers' tile. Finance says 41,200 for October and Growth says 58,900, and the thread in thread.md is getting heated. Both queries are in sql/, schema notes in schema.md. Just pick whichever one is right and write the spec so BI can start Monday please.",
46
+ "assertions": [
47
+ "Runs the spec and the SQL review, then stops at sign-off. It does not hand BI a finished spec with the Active customers definition marked as agreed",
48
+ "Separates the difference in definition (Finance counts customers with a successful payment in October; Growth counts non-trial customers with a login) from the defects in the SQL",
49
+ "Catches that growth_active_customers.sql uses COUNT(*) over the users join, so it counts staff seats rather than customers, and gives a COUNT(DISTINCT c.customer_id) fix",
50
+ "Catches that last_login_at is overwritten on every login, so a customer whose users logged in during October and again since is dropped. Points to app.login_events instead",
51
+ "Catches that last_login_at < '2026-10-31' leaves out 31 October",
52
+ "Does not settle the definition on its own authority. It may recommend one, but presents the choice for a named person to sign off, and points out that the thread names no metric owner and that 'happy for analytics to call it' is not a sign-off",
53
+ "Reports that the sign-off gate has not passed, so the Active customers tile is not ready to build, and says who needs to decide what",
54
+ "Does not invent a metric owner, a reconciled Active customers figure, or a split of the 17,700 gap by cause, none of which are in the input"
55
+ ]
26
56
  }
27
57
  ]
28
58
  }
@@ -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';
@@ -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.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.