skilldrop-cli 0.16.29 → 0.16.31

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 (231) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +10 -4
  3. package/model-routing.json +24 -0
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
  6. package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
  7. package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
  8. package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
  9. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
  10. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
  11. package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
  12. package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
  13. package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
  14. package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
  15. package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
  16. package/packs/dev-team/loops/build/LOOP.md +4 -2
  17. package/packs/dev-team/loops/build/loop.json +2 -2
  18. package/packs/dev-team/loops/release/LOOP.md +7 -5
  19. package/packs/dev-team/loops/release/loop.json +2 -2
  20. package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
  21. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  22. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  113. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  114. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  115. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  116. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  117. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  118. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  119. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  120. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  154. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  155. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  156. package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
  157. package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
  158. package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
  159. package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
  160. package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
  161. package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
  162. package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
  163. package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
  164. package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
  165. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
  166. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
  167. package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
  168. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
  169. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
  170. package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
  171. package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
  172. package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
  173. package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
  174. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
  175. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
  176. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
  177. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
  178. package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
  179. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
  180. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
  181. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
  182. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
  183. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
  184. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
  185. package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
  186. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
  187. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
  188. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
  189. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
  190. package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
  191. package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
  192. package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
  193. package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
  194. package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
  195. package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
  196. package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
  197. package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
  198. package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
  199. package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
  200. package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
  201. package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
  202. package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
  203. package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
  204. package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
  205. package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
  206. package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
  207. package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
  208. package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
  209. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
  210. package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
  211. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  212. package/packs/product-manager/loops/discover/loop.json +2 -2
  213. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  214. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  215. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  216. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  217. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  218. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  219. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  220. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  221. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  222. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  223. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  224. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  225. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  226. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  227. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  228. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  229. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  230. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  231. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
@@ -0,0 +1,233 @@
1
+ """Invoice construction for the subscription billing service.
2
+
3
+ Originally written for monthly plans only. Annual plans, usage add-ons,
4
+ coupons, regional tax and credit notes were bolted on over time.
5
+ """
6
+
7
+ import datetime
8
+ from decimal import Decimal
9
+
10
+ from billing.db import fetch_customer, fetch_plan, fetch_usage, save_invoice
11
+ from billing.coupons import lookup_coupon
12
+ from billing.fx import convert
13
+
14
+ TAX_RATES = {
15
+ "GB": 0.20,
16
+ "DE": 0.19,
17
+ "FR": 0.20,
18
+ "IE": 0.23,
19
+ "US-CA": 0.0725,
20
+ "US-NY": 0.04,
21
+ "US-TX": 0.0625,
22
+ }
23
+
24
+ ADDON_PRICES = {
25
+ "extra_seat": 9.0,
26
+ "sso": 49.0,
27
+ "audit_log": 19.0,
28
+ "priority_support": 99.0,
29
+ }
30
+
31
+ USAGE_TIERS = [
32
+ (0, 10000, 0.0),
33
+ (10000, 100000, 0.002),
34
+ (100000, None, 0.0015),
35
+ ]
36
+
37
+
38
+ def _round_money(value):
39
+ # NOTE: float round(), not Decimal quantize. Known to be off by a cent
40
+ # on some half-cent values. Finance reconciles by hand.
41
+ return round(value, 2)
42
+
43
+
44
+ def _tax_region(customer):
45
+ if customer["country"] == "US":
46
+ return "US-" + (customer.get("state") or "")
47
+ return customer["country"]
48
+
49
+
50
+ def build_invoice(customer_id, period_start, period_end, line_items=None,
51
+ coupon_code=None, currency=None, dry_run=False,
52
+ is_credit_note=False, original_invoice=None):
53
+ """Build (and unless dry_run, persist) an invoice.
54
+
55
+ line_items, if passed, is a list of dicts the caller has pre-populated
56
+ (used by partner statements and manual adjustments).
57
+ """
58
+ customer = fetch_customer(customer_id)
59
+ if customer is None:
60
+ raise ValueError("unknown customer %s" % customer_id)
61
+
62
+ plan = fetch_plan(customer["plan_id"])
63
+ if line_items is None:
64
+ line_items = []
65
+
66
+ if currency is None:
67
+ currency = customer.get("currency", "GBP")
68
+
69
+ # --- plan charge -------------------------------------------------
70
+ if plan["interval"] == "month":
71
+ days_in_period = (period_end - period_start).days
72
+ if days_in_period < 28:
73
+ # proration for mid-cycle upgrades
74
+ plan_amount = plan["price"] * days_in_period / 30
75
+ else:
76
+ plan_amount = plan["price"]
77
+ elif plan["interval"] == "year":
78
+ if period_start.month == customer["anniversary_month"]:
79
+ plan_amount = plan["price"]
80
+ else:
81
+ plan_amount = 0
82
+ else:
83
+ raise ValueError("bad interval %s" % plan["interval"])
84
+
85
+ if plan_amount:
86
+ line_items.append({
87
+ "kind": "plan",
88
+ "description": plan["name"],
89
+ "amount": plan_amount,
90
+ })
91
+
92
+ # --- add-ons -----------------------------------------------------
93
+ for addon in customer.get("addons", []):
94
+ if addon["code"] not in ADDON_PRICES:
95
+ continue
96
+ qty = addon.get("quantity", 1)
97
+ if addon["code"] == "extra_seat":
98
+ included = plan.get("included_seats", 0)
99
+ qty = max(0, qty - included)
100
+ if qty == 0:
101
+ continue
102
+ amount = ADDON_PRICES[addon["code"]] * qty
103
+ if plan["interval"] == "year":
104
+ amount = amount * 12 * 0.9 # annual add-ons get 10% off
105
+ line_items.append({
106
+ "kind": "addon",
107
+ "description": addon["code"],
108
+ "quantity": qty,
109
+ "amount": amount,
110
+ })
111
+
112
+ # --- usage -------------------------------------------------------
113
+ usage = fetch_usage(customer_id, period_start, period_end)
114
+ if usage:
115
+ units = usage["api_calls"]
116
+ usage_amount = 0.0
117
+ for low, high, unit_price in USAGE_TIERS:
118
+ if units <= low:
119
+ break
120
+ top = units if high is None else min(units, high)
121
+ usage_amount += (top - low) * unit_price
122
+ if usage_amount > 0:
123
+ line_items.append({
124
+ "kind": "usage",
125
+ "description": "API calls (%d)" % units,
126
+ "amount": usage_amount,
127
+ })
128
+
129
+ # --- coupons -----------------------------------------------------
130
+ discount = 0.0
131
+ if coupon_code:
132
+ coupon = lookup_coupon(coupon_code)
133
+ if coupon and coupon["active"]:
134
+ if coupon.get("expires") and coupon["expires"] < period_start:
135
+ pass
136
+ elif coupon["type"] == "percent":
137
+ base = sum(li["amount"] for li in line_items
138
+ if li["kind"] in ("plan", "addon"))
139
+ discount = base * coupon["value"] / 100.0
140
+ elif coupon["type"] == "fixed":
141
+ discount = coupon["value"]
142
+ if coupon.get("currency") and coupon["currency"] != currency:
143
+ discount = convert(discount, coupon["currency"], currency)
144
+ elif coupon["type"] == "first_month_free":
145
+ if customer.get("invoice_count", 0) == 0:
146
+ discount = sum(li["amount"] for li in line_items
147
+ if li["kind"] == "plan")
148
+ if discount:
149
+ line_items.append({
150
+ "kind": "discount",
151
+ "description": "Coupon %s" % coupon_code,
152
+ "amount": -discount,
153
+ })
154
+
155
+ # --- tax ---------------------------------------------------------
156
+ subtotal = sum(li["amount"] for li in line_items)
157
+ region = _tax_region(customer)
158
+ rate = TAX_RATES.get(region, 0.0)
159
+ if customer.get("tax_exempt"):
160
+ rate = 0.0
161
+ if customer.get("vat_number") and region in ("DE", "FR", "IE"):
162
+ # EU B2B: reverse charge, but only if not same country as us
163
+ rate = 0.0
164
+ tax = subtotal * rate
165
+ if tax:
166
+ # Appended to the caller's list on purpose? Nobody remembers.
167
+ line_items.append({
168
+ "kind": "tax",
169
+ "description": "Tax (%s @ %.2f%%)" % (region, rate * 100),
170
+ "amount": tax,
171
+ })
172
+
173
+ # --- currency ----------------------------------------------------
174
+ if currency != "GBP":
175
+ for li in line_items:
176
+ li["amount"] = convert(li["amount"], "GBP", currency)
177
+ subtotal = convert(subtotal, "GBP", currency)
178
+ tax = convert(tax, "GBP", currency)
179
+
180
+ total = subtotal + tax
181
+
182
+ # --- credit notes ------------------------------------------------
183
+ if is_credit_note:
184
+ if original_invoice is None:
185
+ raise ValueError("credit note needs original_invoice")
186
+ for li in line_items:
187
+ li["amount"] = -li["amount"]
188
+ subtotal = -subtotal
189
+ tax = -tax
190
+ total = -total
191
+ if abs(total) > abs(original_invoice["total"]):
192
+ raise ValueError("credit note exceeds original")
193
+
194
+ # --- rounding ----------------------------------------------------
195
+ for li in line_items:
196
+ li["amount"] = _round_money(li["amount"])
197
+ subtotal = _round_money(subtotal)
198
+ tax = _round_money(tax)
199
+ total = _round_money(total)
200
+
201
+ invoice = {
202
+ "customer_id": customer_id,
203
+ "period_start": period_start.isoformat(),
204
+ "period_end": period_end.isoformat(),
205
+ "currency": currency,
206
+ "lines": line_items,
207
+ "subtotal": subtotal,
208
+ "tax": tax,
209
+ "total": total,
210
+ "credit_note": is_credit_note,
211
+ "created_at": datetime.datetime.utcnow().isoformat(),
212
+ }
213
+ if original_invoice is not None:
214
+ invoice["original_invoice_id"] = original_invoice["id"]
215
+
216
+ if not dry_run:
217
+ invoice["id"] = save_invoice(invoice)
218
+ return invoice
219
+
220
+
221
+ def preview_invoice(customer_id, period_start, period_end, coupon_code=None):
222
+ return build_invoice(customer_id, period_start, period_end,
223
+ coupon_code=coupon_code, dry_run=True)
224
+
225
+
226
+ def invoice_to_decimal(invoice):
227
+ """Used by the ledger export, which wants Decimal."""
228
+ out = dict(invoice)
229
+ for key in ("subtotal", "tax", "total"):
230
+ out[key] = Decimal(str(invoice[key]))
231
+ out["lines"] = [dict(li, amount=Decimal(str(li["amount"])))
232
+ for li in invoice["lines"]]
233
+ return out
@@ -0,0 +1,29 @@
1
+ """Monthly partner (reseller) statements.
2
+
3
+ Resellers get a statement per sub-account showing what was billed and
4
+ how much tax was charged, so they can reclaim it.
5
+ """
6
+
7
+ from billing.invoice import build_invoice
8
+ from partners.db import sub_accounts_for, save_statement
9
+
10
+
11
+ def partner_statement(partner_id, period_start, period_end):
12
+ rows = []
13
+ for account in sub_accounts_for(partner_id):
14
+ lines = [{
15
+ "kind": "partner_margin",
16
+ "description": "Reseller margin",
17
+ "amount": -account["margin"],
18
+ }]
19
+ build_invoice(account["customer_id"], period_start, period_end,
20
+ line_items=lines, dry_run=True)
21
+ # build_invoice appends the tax line onto `lines`; we read it back
22
+ # from our own list rather than from the returned invoice.
23
+ tax_line = next((li for li in lines if li["kind"] == "tax"), None)
24
+ rows.append({
25
+ "customer_id": account["customer_id"],
26
+ "net": sum(li["amount"] for li in lines if li["kind"] != "tax"),
27
+ "tax": tax_line["amount"] if tax_line else 0.0,
28
+ })
29
+ return save_statement(partner_id, period_start, period_end, rows)
@@ -0,0 +1,13 @@
1
+ from billing.payments import charge_card, PaymentDeclined
2
+ import pytest
3
+
4
+
5
+ def test_charge_card_success(fake_gateway):
6
+ fake_gateway.next_response = {"status": "ok", "id": "ch_1"}
7
+ assert charge_card("cus_1", 1000, "GBP")["id"] == "ch_1"
8
+
9
+
10
+ def test_charge_card_declined(fake_gateway):
11
+ fake_gateway.next_response = {"status": "declined"}
12
+ with pytest.raises(PaymentDeclined):
13
+ charge_card("cus_1", 1000, "GBP")
@@ -0,0 +1,5 @@
1
+ CI right now (from #eng-ci pinned msg):
2
+ - unit: ~3 min
3
+ - e2e (playwright, staging SMTP sink): ~38 min, flaky maybe 1 in 6 runs
4
+ - deploy to prod is manual approve after e2e green
5
+ - we deploy ~2x a week
@@ -0,0 +1,4 @@
1
+ #88 feat(orders): "order delayed" email touches src/notify/legacyMailer.ts (new TemplateName + subject), src/orders/onOrderDelayed.ts -- wanted for peak season
2
+ #91 chore(notify): move templates to MJML touches src/notify/templates/*, src/notify/legacyMailer.ts (loadTemplate) -- design team, 2 wks old
3
+ #93 feat(auth): magic-link login touches src/auth/*, calls mailer.send with new template "magic_link"
4
+ #95 fix(ci): retry flaky e2e touches .github/workflows/ci.yml
@@ -0,0 +1,36 @@
1
+ // Nightly weekly-digest sender. Runs from the ops cron box, not the app.
2
+ // Copied the SMTP bits out of legacyMailer so this could run without
3
+ // building the TS app (2023).
4
+
5
+ const nodemailer = require("nodemailer");
6
+ const fs = require("fs");
7
+ const { getDigestRecipients } = require("../dist/lib/digest");
8
+
9
+ const transport = nodemailer.createTransport({
10
+ host: process.env.SMTP_HOST,
11
+ port: Number(process.env.SMTP_PORT || 25),
12
+ auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
13
+ });
14
+
15
+ function render(tpl, vars) {
16
+ return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k) => (vars[k] == null ? "" : String(vars[k])));
17
+ }
18
+
19
+ async function main() {
20
+ const tpl = fs.readFileSync(__dirname + "/../src/notify/templates/weekly_digest.html", "utf8");
21
+ const recipients = await getDigestRecipients();
22
+ for (const r of recipients) {
23
+ // no suppression-list check here
24
+ await transport.sendMail({
25
+ from: process.env.SMTP_FROM,
26
+ to: r.email,
27
+ subject: "Your week in review",
28
+ html: render(tpl, r.vars),
29
+ });
30
+ }
31
+ }
32
+
33
+ main().catch((e) => {
34
+ console.error(e);
35
+ process.exit(1);
36
+ });
@@ -0,0 +1,16 @@
1
+ import { mailer } from "../notify";
2
+ import { users } from "../lib/users";
3
+ import { tokens } from "../lib/tokens";
4
+
5
+ export async function requestPasswordReset(email: string) {
6
+ const user = await users.findByEmail(email);
7
+ // Always respond the same way whether or not the user exists.
8
+ if (!user) return { ok: true };
9
+ const token = await tokens.issueReset(user.id);
10
+ await mailer.send({
11
+ to: user.email,
12
+ template: "password_reset",
13
+ vars: { resetUrl: `https://app.example.test/reset?t=${token}` },
14
+ });
15
+ return { ok: true };
16
+ }
@@ -0,0 +1,2 @@
1
+ export { mailer, LegacyMailer, render } from "./legacyMailer";
2
+ export type { TemplateName, SendOptions } from "./legacyMailer";
@@ -0,0 +1,149 @@
1
+ // LegacyMailer — talks SMTP directly to the old relay.
2
+ // Written 2022. Do not add features here (says everyone, then adds features).
3
+
4
+ import nodemailer from "nodemailer";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { logger } from "../lib/logger";
8
+ import { config } from "../lib/config";
9
+
10
+ export type TemplateName =
11
+ | "order_shipped"
12
+ | "order_confirmed"
13
+ | "password_reset"
14
+ | "welcome"
15
+ | "weekly_digest";
16
+
17
+ export interface SendOptions {
18
+ to: string | string[];
19
+ template: TemplateName;
20
+ vars: Record<string, unknown>;
21
+ replyTo?: string;
22
+ attachments?: { filename: string; content: Buffer }[];
23
+ bccOps?: boolean;
24
+ }
25
+
26
+ const TEMPLATE_DIR = path.join(__dirname, "templates");
27
+ const templateCache: Record<string, string> = {};
28
+
29
+ let transport: nodemailer.Transporter | null = null;
30
+
31
+ function getTransport(): nodemailer.Transporter {
32
+ if (!transport) {
33
+ transport = nodemailer.createTransport({
34
+ host: config.smtp.host,
35
+ port: config.smtp.port,
36
+ secure: false,
37
+ auth: { user: config.smtp.user, pass: config.smtp.pass },
38
+ pool: true,
39
+ maxConnections: 3,
40
+ });
41
+ }
42
+ return transport;
43
+ }
44
+
45
+ function loadTemplate(name: TemplateName): string {
46
+ if (templateCache[name]) return templateCache[name];
47
+ const file = path.join(TEMPLATE_DIR, `${name}.html`);
48
+ const body = fs.readFileSync(file, "utf8");
49
+ templateCache[name] = body;
50
+ return body;
51
+ }
52
+
53
+ // Very small mustache-ish renderer. Unknown vars render as empty string.
54
+ export function render(tpl: string, vars: Record<string, unknown>): string {
55
+ return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, key) => {
56
+ const v = vars[key];
57
+ if (v === undefined || v === null) return "";
58
+ return String(v);
59
+ });
60
+ }
61
+
62
+ function subjectFor(template: TemplateName, vars: Record<string, unknown>): string {
63
+ switch (template) {
64
+ case "order_shipped":
65
+ return `Your order ${vars.orderNumber} is on its way`;
66
+ case "order_confirmed":
67
+ return `We've got your order ${vars.orderNumber}`;
68
+ case "password_reset":
69
+ return "Reset your password";
70
+ case "welcome":
71
+ return `Welcome, ${vars.firstName ?? "there"}!`;
72
+ case "weekly_digest":
73
+ return "Your week in review";
74
+ }
75
+ }
76
+
77
+ function normaliseRecipients(to: string | string[]): string[] {
78
+ const list = Array.isArray(to) ? to : to.split(",");
79
+ return list
80
+ .map((s) => s.trim().toLowerCase())
81
+ .filter((s) => s.length > 0);
82
+ }
83
+
84
+ function isSuppressed(address: string): boolean {
85
+ // suppression list is a flat file synced nightly from the old provider
86
+ try {
87
+ const raw = fs.readFileSync(config.smtp.suppressionFile, "utf8");
88
+ return raw.split("\n").includes(address);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ export class LegacyMailer {
95
+ private sentCount = 0;
96
+
97
+ async send(opts: SendOptions): Promise<boolean> {
98
+ const recipients = normaliseRecipients(opts.to).filter((r) => !isSuppressed(r));
99
+ if (recipients.length === 0) {
100
+ logger.info("legacyMailer: all recipients suppressed", { template: opts.template });
101
+ return true;
102
+ }
103
+
104
+ const html = render(loadTemplate(opts.template), opts.vars);
105
+ const subject = subjectFor(opts.template, opts.vars);
106
+
107
+ const message: nodemailer.SendMailOptions = {
108
+ from: config.smtp.from,
109
+ to: recipients.join(", "),
110
+ subject,
111
+ html,
112
+ replyTo: opts.replyTo,
113
+ attachments: opts.attachments,
114
+ };
115
+ if (opts.bccOps) {
116
+ message.bcc = config.smtp.opsAddress;
117
+ }
118
+
119
+ try {
120
+ await getTransport().sendMail(message);
121
+ this.sentCount++;
122
+ return true;
123
+ } catch (err) {
124
+ // TODO(2022): surface this properly. For now don't break checkout.
125
+ logger.warn("legacyMailer: send failed", { err, template: opts.template });
126
+ return true;
127
+ }
128
+ }
129
+
130
+ async sendTemplate(template: TemplateName, to: string, vars: Record<string, unknown>) {
131
+ return this.send({ template, to, vars });
132
+ }
133
+
134
+ async sendBulk(template: TemplateName, recipients: { to: string; vars: Record<string, unknown> }[]) {
135
+ const results: boolean[] = [];
136
+ for (const r of recipients) {
137
+ // sequential on purpose: the relay rate-limits at ~5/s
138
+ results.push(await this.send({ template, to: r.to, vars: r.vars }));
139
+ await new Promise((res) => setTimeout(res, 200));
140
+ }
141
+ return results;
142
+ }
143
+
144
+ stats() {
145
+ return { sent: this.sentCount };
146
+ }
147
+ }
148
+
149
+ export const mailer = new LegacyMailer();
@@ -0,0 +1,15 @@
1
+ import { mailer } from "../notify";
2
+ import { retryQueue } from "../lib/retryQueue";
3
+ import { Order } from "./types";
4
+
5
+ export async function onOrderShipped(order: Order) {
6
+ const sent = await mailer.sendTemplate("order_shipped", order.customerEmail, {
7
+ orderNumber: order.number,
8
+ trackingUrl: order.trackingUrl,
9
+ firstName: order.customerFirstName,
10
+ });
11
+ if (!sent) {
12
+ // never seen this fire in prod logs, but keep it just in case
13
+ await retryQueue.push({ kind: "email", template: "order_shipped", orderId: order.id });
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ import { render } from "../../src/notify/legacyMailer";
2
+
3
+ describe("render", () => {
4
+ it("substitutes vars", () => {
5
+ expect(render("Hi {{ name }}", { name: "Ada" })).toBe("Hi Ada");
6
+ });
7
+ });
@@ -0,0 +1,29 @@
1
+ package billing
2
+
3
+ import org.springframework.scheduling.annotation.Scheduled
4
+ import org.springframework.stereotype.Component
5
+ import org.springframework.transaction.annotation.Transactional
6
+ import java.time.Clock
7
+ import java.time.LocalDate
8
+
9
+ @Component
10
+ class DunningJob(
11
+ private val invoiceService: InvoiceService,
12
+ private val notifier: DunningNotifier,
13
+ private val clock: Clock,
14
+ ) {
15
+
16
+ @Scheduled(cron = "0 0 6 * * *")
17
+ @Transactional
18
+ fun run() {
19
+ val today = LocalDate.now(clock)
20
+ invoiceService.applyLateFees(today)
21
+
22
+ invoiceService.findOverdue(today).forEach { invoice ->
23
+ invoice.status = InvoiceStatus.IN_DUNNING
24
+ invoice.dunningLevel = invoice.dunningLevel + 1
25
+ invoice.lastDunnedOn = today
26
+ notifier.send(invoice.customer, invoice, invoice.dunningLevel)
27
+ }
28
+ }
29
+ }