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
@@ -1,102 +0,0 @@
1
- # lean-canvas reference
2
-
3
- Long-form material for the steps in `SKILL.md`. The canvas is Ash Maurya's adaptation of the
4
- Business Model Canvas (*Running Lean*); the interview rules follow Rob Fitzpatrick's *The Mom
5
- Test*.
6
-
7
- ## The nine boxes, in the order to fill them
8
-
9
- | Box | What goes in | Common wrong entry |
10
- |---|---|---|
11
- | Problem | The top 1–3 problems, and how the early adopter copes today (existing alternatives) | A solution phrased as a problem: "no app for X" |
12
- | Customer segments | Who has the problem; the **early adopter** named separately, narrow enough to find by name | "Small businesses", "everyone who…" |
13
- | Unique value proposition | One sentence: why this, for the early adopter, instead of what they do today | A feature list |
14
- | Solution | The smallest thing that addresses each problem | A roadmap |
15
- | Channels | The paths to the early adopter, free and paid | Channels that reach the later segment |
16
- | Revenue streams | Price, who pays, how often | "Freemium" with no paid tier defined |
17
- | Cost structure | Cost to serve one customer, fixed costs, acquisition cost | Fixed costs only |
18
- | Key metrics | The 1–3 numbers that show the model works (activation, retention, paid conversion) | Downloads, page views |
19
- | Unfair advantage | What a funded competitor can't copy or buy within a year | A feature, "AI", "first mover", "passion" |
20
-
21
- **Internal products.** Revenue streams = the budget line or measured saving that keeps it
22
- funded. Customer = the team that has to change how it works; early adopter = the one team
23
- with the worst version of the problem and a lead who asked for help. Channel = how that team
24
- hears about and gets onto the tool (a mandate, a team lead, an existing workflow it plugs
25
- into). Unfair advantage can be real here: access to internal data or a mandate no vendor
26
- has. Still test whether the team will change its habits.
27
-
28
- ## Grading evidence
29
-
30
- | Signal | Grade |
31
- |---|---|
32
- | Paid, pre-ordered, signed a letter of intent, or switched from a current tool | Behaviour — strongest |
33
- | Describes a recent, specific instance of the problem unprompted, and what it cost | Behaviour about the past — strong |
34
- | Built a workaround (spreadsheet, hired someone) | Behaviour — strong |
35
- | "I'd definitely use it", "great idea", survey interest | Opinion — weak, counts as assumption |
36
- | Industry statistic or report | Sizes the market; says nothing about this segment's willingness to pay |
37
- | Any of the above from a segment other than the early adopter | Counts as assumption for the early adopter |
38
-
39
- ## Scoring risk
40
-
41
- | | Fatal (3) | Pivot (2) | Tune (1) |
42
- |---|---|---|---|
43
- | **High (3)** — no evidence, wrong segment, opinion only | 9 | 6 | 3 |
44
- | **Medium (2)** — right segment, few people or indirect | 6 | 4 | 2 |
45
- | **Low (1)** — observed behaviour, right segment | 3 | 2 | 1 |
46
-
47
- Ties: rank the assumption whose test is cheaper first; it clears the way for the rest.
48
-
49
- Why problem, customer and channel usually top the list: a wrong solution is fixed by
50
- rebuilding it, which a team can do in weeks. A wrong problem or an unreachable customer means
51
- the next build lands on nobody, and no amount of engineering moves it.
52
-
53
- ## Test menu, cheapest first
54
-
55
- | Test | Proves wrong | Typical sample | Typical cost and time |
56
- |---|---|---|---|
57
- | Problem interview | The problem, the segment | 10–20 early adopters | $0–$200, 1–2 weeks |
58
- | Channel smoke test | The channel and its cost | $100–$500 of spend or one week of outreach on one channel | the spend, 1 week |
59
- | Landing page with a price | The UVP and the price | 100+ visitors from the stated channel | $100–$500, 1–2 weeks |
60
- | Concierge run (deliver it by hand) | The solution and retention | 3–10 customers | founder time, 2–4 weeks |
61
- | Pre-sale or letter of intent | Willingness to pay | 5–20 asks | $0, 2–4 weeks |
62
- | Wizard of Oz (looks automated, done by hand) | The solution at small volume | 10–30 uses | founder time, 2–4 weeks |
63
-
64
- A survey and "build an MVP and see" are not on the menu: the first measures opinion, the
65
- second costs months to learn what an interview learns in a week.
66
-
67
- ## Setting the pass/fail line
68
-
69
- - **Write it before the test runs**, as a count: "fewer than 4 of 10". A line set after the
70
- results arrive moves to fit them.
71
- - **Take it from the numbers when they exist.** Channel test fail line = the most a customer
72
- can cost to acquire (lifetime margin ÷ 3 for a business that has to cover fixed costs;
73
- lifetime margin at break-even). Landing-page fail line = the conversion that would
74
- produce that cost per customer at the channel's cost per visitor.
75
- - **No numbers yet:** a problem interview passes when at least 4 in 10 describe a recent
76
- instance unprompted; a pre-sale passes when at least 1 in 5 pays or signs.
77
- - **Say what failure changes:** which box is rewritten, and what the next test is.
78
-
79
- ## Problem-interview rules
80
-
81
- 1. Ask about the past, never the future. ✅ "When did that last happen? What did you do?"
82
- ❌ "Would you use an app that…?"
83
- 2. Don't pitch until the end, if at all. The moment the idea is described, people are polite.
84
- 3. Ask what it cost them: time, money, a lost customer.
85
- 4. Ask what they do about it today and what that costs. No workaround is a warning sign.
86
- 5. Count who raised the problem **before** it was named, separately from who agreed once asked.
87
-
88
- Five-question script: "Walk me through your last [day/week/job]." / "What was the hardest
89
- part?" / "When did [problem area] last go wrong? What happened?" / "What did you do about
90
- it, and what did that cost?" / "What have you tried to fix it?"
91
-
92
- ## What counts as an unfair advantage
93
-
94
- Counts: insider information, an existing audience or customer base, a network effect,
95
- a community, exclusive contracts or access, regulatory permission others lack, a team
96
- member whose name opens doors in this market, internal data or a mandate (internal products).
97
-
98
- Doesn't count: a feature, an algorithm, "AI", a patent pending on a feature, first-mover,
99
- better UX, lower price, hard work, passion. These go in Solution or UVP.
100
-
101
- "None yet" is the most common correct answer at this stage. Name a candidate the idea
102
- could grow into, if the input supports one.
@@ -1,211 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Unit-economics sanity check for a lean canvas.
3
-
4
- Stdlib only; runs on Python 3.9+. Computes, never estimates:
5
-
6
- * contribution (gross margin) per customer per period, and as a % of price;
7
- * customer lifetime in periods (given directly, or 1 / churn);
8
- * lifetime gross margin (LTV) and LTV:CAC;
9
- * CAC payback in periods, and whether it is longer than the lifetime;
10
- * the most a customer can cost to acquire: at break-even (LTV) and at 3:1 (LTV / 3);
11
- * break-even customers for the fixed costs, and the customers a stated
12
- market can supply, when those are given.
13
-
14
- All money figures share one currency and one period (default "month"). Convert
15
- an annual price to monthly before passing it, or pass --period year with
16
- annual figures throughout.
17
-
18
- Usage:
19
- python3 unit_economics.py --price 29 --variable-cost 6 --cac 400 \
20
- --lifetime 9 --fixed 4300
21
- python3 unit_economics.py --price 29 --variable-cost 6 --cac 400 --churn 0.11
22
- python3 unit_economics.py --json inputs.json [-o out.md]
23
- echo '{"price": 29, "variable_cost": 6, "cac": 400, "lifetime": 9}' | \
24
- python3 unit_economics.py --json -
25
-
26
- JSON keys mirror the flags: price, variable_cost, cac, lifetime, churn,
27
- fixed, market_customers, period. Flags given on the command line override
28
- the JSON.
29
-
30
- Exit codes: 0 ok, 2 bad input.
31
- """
32
-
33
- from __future__ import annotations
34
-
35
- import argparse
36
- import json
37
- import sys
38
-
39
- KEYS = ("price", "variable_cost", "cac", "lifetime", "churn", "fixed", "market_customers")
40
-
41
-
42
- def fail(msg: str) -> None:
43
- print(f"error: {msg}", file=sys.stderr)
44
- sys.exit(2)
45
-
46
-
47
- def load_json(path: str) -> dict:
48
- try:
49
- if path == "-":
50
- data = json.load(sys.stdin)
51
- else:
52
- with open(path, encoding="utf-8") as fh:
53
- data = json.load(fh)
54
- except FileNotFoundError:
55
- fail(f"no such file: {path}")
56
- except json.JSONDecodeError as exc:
57
- fail(f"not valid JSON ({exc})")
58
- if not isinstance(data, dict):
59
- fail("JSON input must be one object")
60
- return data
61
-
62
-
63
- def number(name: str, value, minimum: float = 0.0, allow_equal: bool = True):
64
- if value is None:
65
- return None
66
- try:
67
- val = float(value)
68
- except (TypeError, ValueError):
69
- fail(f"{name} must be a number, got {value!r}")
70
- if val < minimum or (not allow_equal and val == minimum):
71
- op = ">=" if allow_equal else ">"
72
- fail(f"{name} must be {op} {minimum:g}, got {val:g}")
73
- return val
74
-
75
-
76
- def money(x: float) -> str:
77
- return f"{x:,.2f}"
78
-
79
-
80
- def compute(inp: dict) -> tuple[list[str], list[str]]:
81
- period = str(inp.get("period") or "month")
82
- price = number("price", inp.get("price"), 0.0, allow_equal=False)
83
- if price is None:
84
- fail("price is required (per customer per period)")
85
- vcost = number("variable_cost", inp.get("variable_cost")) or 0.0
86
- cac = number("cac", inp.get("cac"))
87
- lifetime = number("lifetime", inp.get("lifetime"), 0.0, allow_equal=False)
88
- churn = inp.get("churn")
89
- fixed = number("fixed", inp.get("fixed"))
90
- market = number("market_customers", inp.get("market_customers"))
91
-
92
- if churn is not None:
93
- churn = number("churn", churn, 0.0, allow_equal=False)
94
- if churn > 1:
95
- if churn <= 100:
96
- churn = churn / 100.0 # "11" meaning 11%
97
- else:
98
- fail("churn must be a fraction (0.11) or a percent (11)")
99
- if lifetime is None:
100
- lifetime = 1.0 / churn
101
-
102
- lines: list[str] = []
103
- flags: list[str] = []
104
-
105
- margin = price - vcost
106
- margin_pct = margin / price * 100
107
- lines.append(f"Price per customer per {period}: {money(price)}")
108
- if inp.get("variable_cost") is None:
109
- lines.append(f"Variable cost per customer per {period}: not given, treated as 0")
110
- flags.append("No cost to serve was given, so every margin figure here is an upper bound.")
111
- else:
112
- lines.append(f"Variable cost per customer per {period}: {money(vcost)}")
113
- lines.append(f"Contribution per customer per {period}: {money(margin)} ({margin_pct:.1f}% gross margin)")
114
- if margin <= 0:
115
- flags.append(f"Each customer costs more to serve than they pay ({money(margin)} per {period}). No volume fixes this.")
116
-
117
- ltv = None
118
- if lifetime is not None:
119
- src = f"1 / churn of {churn * 100:.1f}%" if churn is not None and inp.get("lifetime") is None else "as given"
120
- lines.append(f"Customer lifetime: {lifetime:.1f} {period}s ({src})")
121
- ltv = margin * lifetime
122
- lines.append(f"Lifetime contribution (LTV): {money(ltv)}")
123
-
124
- if cac is not None:
125
- lines.append(f"Customer acquisition cost (CAC): {money(cac)}")
126
- if margin > 0:
127
- payback = cac / margin
128
- lines.append(f"CAC payback: {payback:.1f} {period}s")
129
- if lifetime is not None and payback > lifetime:
130
- flags.append(
131
- f"CAC payback ({payback:.1f} {period}s) is longer than the customer lifetime "
132
- f"({lifetime:.1f} {period}s): every acquired customer loses {money(cac - ltv)}."
133
- )
134
- if ltv is not None:
135
- ratio = ltv / cac if cac > 0 else float("inf")
136
- lines.append(f"LTV:CAC: {ratio:.2f}" if cac > 0 else "LTV:CAC: no acquisition cost given as 0")
137
- if cac > 0 and ratio < 1:
138
- flags.append(f"LTV:CAC is {ratio:.2f}: acquisition destroys money before fixed costs.")
139
- elif cac > 0 and ratio < 3:
140
- flags.append(f"LTV:CAC is {ratio:.2f}, under the 3:1 most subscription businesses need to cover fixed costs.")
141
-
142
- if ltv is not None and ltv > 0:
143
- lines.append(f"Most a customer can cost to acquire: {money(ltv)} at break-even, {money(ltv / 3)} at 3:1")
144
-
145
- if fixed is not None:
146
- lines.append(f"Fixed costs per {period}: {money(fixed)}")
147
- if margin > 0:
148
- be = fixed / margin
149
- be_whole = int(be) if be == int(be) else int(be) + 1
150
- lines.append(f"Break-even customers (active at once): {be_whole:,}")
151
- if market is not None:
152
- lines.append(f"Customers the stated market holds: {int(market):,}")
153
- if be_whole > market:
154
- flags.append(
155
- f"Break-even needs {be_whole:,} active customers but the stated market holds {int(market):,}."
156
- )
157
- else:
158
- share = be_whole / market * 100 if market else 0.0 # 0 needed of a 0 market
159
- lines.append(f"Break-even share of the stated market: {share:.1f}%")
160
- if share >= 25:
161
- flags.append(
162
- f"Break-even needs {share:.0f}% of the stated market as paying customers at the same time."
163
- )
164
- if cac is not None and ltv is not None and ltv <= cac:
165
- flags.append("Break-even customers cannot be reached through paid acquisition at this CAC: each one adds a loss.")
166
-
167
- missing = [k for k in ("variable_cost", "cac", "lifetime/churn", "fixed")
168
- if (k == "lifetime/churn" and lifetime is None)
169
- or (k != "lifetime/churn" and inp.get(k) is None)]
170
- if missing:
171
- lines.append("Not computed (input missing): " + ", ".join(missing))
172
- return lines, flags
173
-
174
-
175
- def main() -> None:
176
- ap = argparse.ArgumentParser(description="Unit-economics sanity check for a lean canvas.")
177
- ap.add_argument("--json", metavar="PATH", help="JSON object of inputs, or - for stdin")
178
- ap.add_argument("--price", help="price per customer per period")
179
- ap.add_argument("--variable-cost", dest="variable_cost", help="cost to serve one customer per period")
180
- ap.add_argument("--cac", help="cost to acquire one paying customer")
181
- ap.add_argument("--lifetime", help="average customer lifetime in periods")
182
- ap.add_argument("--churn", help="churn per period, as 0.11 or 11")
183
- ap.add_argument("--fixed", help="fixed costs per period")
184
- ap.add_argument("--market-customers", dest="market_customers", help="customers the stated market holds")
185
- ap.add_argument("--period", help='period label, default "month"')
186
- ap.add_argument("-o", "--output", help="write the report to this path instead of stdout")
187
- args = ap.parse_args()
188
-
189
- inp: dict = load_json(args.json) if args.json else {}
190
- unknown = set(inp) - set(KEYS) - {"period"}
191
- if unknown:
192
- fail("unknown JSON keys: " + ", ".join(sorted(unknown)))
193
- for key in KEYS + ("period",):
194
- val = getattr(args, key)
195
- if val is not None:
196
- inp[key] = val
197
-
198
- lines, flags = compute(inp)
199
- out = ["Unit economics", ""] + [f"- {ln}" for ln in lines]
200
- out += ["", "Where the numbers don't close:" if flags else "Where the numbers don't close: none found."]
201
- out += [f"- {f}" for f in flags]
202
- text = "\n".join(out) + "\n"
203
- if args.output:
204
- with open(args.output, "w", encoding="utf-8") as fh:
205
- fh.write(text)
206
- else:
207
- sys.stdout.write(text)
208
-
209
-
210
- if __name__ == "__main__":
211
- main()