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,453 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build an AI usage report from a usage-event log.
3
-
4
- Reads a CSV or JSONL file of usage events and writes a markdown report.
5
- See ../templates/usage-event-schema.md for the input schema.
6
-
7
- Usage:
8
- python3 build_report.py events.csv --view per-user --period week -o report.md
9
- python3 build_report.py events.jsonl --view team-rollup --from 2026-05-01 --to 2026-05-31 -o report.md
10
- python3 build_report.py events.csv --view effectiveness --period week -o report.md
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import argparse
16
- import csv
17
- import json
18
- import math
19
- import sys
20
- from collections import Counter, defaultdict
21
- from datetime import datetime, timedelta, timezone
22
- from pathlib import Path
23
- from statistics import median
24
-
25
- VIEWS = ("per-user", "team-rollup", "effectiveness")
26
- MIN_USERS_FOR_ROLLUP = 5
27
-
28
-
29
- def load_events(path: Path) -> list[dict]:
30
- suffix = path.suffix.lower()
31
- try:
32
- if suffix == ".csv":
33
- with path.open(newline="", encoding="utf-8-sig") as f:
34
- return list(csv.DictReader(f))
35
- if suffix in (".jsonl", ".ndjson"):
36
- events = []
37
- with path.open(encoding="utf-8-sig") as f:
38
- for n, line in enumerate(f, 1):
39
- s = line.strip()
40
- if s:
41
- try:
42
- event = json.loads(s)
43
- except ValueError as exc:
44
- raise SystemExit(f"error: {path.name} line {n} is not valid JSON ({exc.msg}); fix or remove that line")
45
- if not isinstance(event, dict):
46
- raise SystemExit(f"error: {path.name} line {n} is not a JSON object; each line must be one event object")
47
- events.append(event)
48
- return events
49
- except UnicodeDecodeError:
50
- raise SystemExit(f"error: {path.name} is not UTF-8 text; save it as UTF-8 and re-run")
51
- raise SystemExit(f"error: unsupported file type {suffix!r} (use .csv or .jsonl)")
52
-
53
-
54
- def parse_ts(s) -> datetime | None:
55
- if not s:
56
- return None
57
- if isinstance(s, datetime):
58
- return s if s.tzinfo else s.replace(tzinfo=timezone.utc)
59
- s = str(s).strip()
60
- if not s:
61
- return None
62
- if s.endswith("Z"):
63
- s = s[:-1] + "+00:00"
64
- try:
65
- dt = datetime.fromisoformat(s)
66
- except ValueError:
67
- # Try date-only
68
- try:
69
- dt = datetime.fromisoformat(s + "T00:00:00+00:00")
70
- except ValueError:
71
- return None
72
- return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
73
-
74
-
75
- def parse_bool(v):
76
- if v is None or v == "":
77
- return None
78
- if isinstance(v, bool):
79
- return v
80
- s = str(v).strip().lower()
81
- if s in ("1", "true", "yes", "y", "t"):
82
- return True
83
- if s in ("0", "false", "no", "n", "f"):
84
- return False
85
- return None
86
-
87
-
88
- def resolve_period(period: str, from_arg: str | None, to_arg: str | None):
89
- if from_arg or to_arg:
90
- if not (from_arg and to_arg):
91
- raise SystemExit("error: --from and --to must be used together")
92
- start = parse_ts(from_arg)
93
- end = parse_ts(to_arg)
94
- if start is None or end is None:
95
- raise SystemExit(f"error: could not parse --from / --to dates")
96
- # If user passed date-only for --to, push to end of day
97
- if len(to_arg) <= 10:
98
- end = end + timedelta(days=1, seconds=-1)
99
- return start, end
100
- now = datetime.now(timezone.utc)
101
- if period == "week":
102
- return now - timedelta(days=7), now
103
- if period == "month":
104
- return now - timedelta(days=30), now
105
- raise SystemExit(f"error: unknown --period {period!r} (use week, month, or --from/--to)")
106
-
107
-
108
- def detect_signals(events: list[dict]) -> dict:
109
- """Which optional fields are populated in the input."""
110
- keys_present = set()
111
- for e in events[:1000]: # sample
112
- for k, v in e.items():
113
- if v not in (None, ""):
114
- keys_present.add(k)
115
- return {
116
- "session_id": "session_id" in keys_present,
117
- "output_consumed": "output_consumed" in keys_present,
118
- "consumed_at": "consumed_at" in keys_present,
119
- "outcome": "outcome" in keys_present,
120
- }
121
-
122
-
123
- def filter_to_period(events: list[dict], start: datetime, end: datetime) -> list[dict]:
124
- out = []
125
- for e in events:
126
- ts = parse_ts(e.get("timestamp"))
127
- if ts and start <= ts <= end:
128
- out.append({**e, "_ts": ts})
129
- return out
130
-
131
-
132
- def aggregate(events: list[dict], signals: dict):
133
- per_user = defaultdict(lambda: {
134
- "invocations": 0,
135
- "sessions": set(),
136
- "tools": set(),
137
- "session_counts": defaultdict(int),
138
- "consumed_known": 0,
139
- "consumed_yes": 0,
140
- })
141
- tool_counter: Counter = Counter()
142
-
143
- for e in events:
144
- u = e.get("user")
145
- if not u:
146
- continue
147
- d = per_user[u]
148
- d["invocations"] += 1
149
- tool = e.get("tool") or "(unknown)"
150
- d["tools"].add(tool)
151
- tool_counter[tool] += 1
152
-
153
- if signals["session_id"]:
154
- sid = e.get("session_id")
155
- if sid:
156
- d["sessions"].add(sid)
157
- d["session_counts"][sid] += 1
158
-
159
- if signals["output_consumed"]:
160
- b = parse_bool(e.get("output_consumed"))
161
- if b is not None:
162
- d["consumed_known"] += 1
163
- if b:
164
- d["consumed_yes"] += 1
165
-
166
- return per_user, tool_counter
167
-
168
-
169
- def fmt_pct(num, denom) -> str:
170
- if not denom:
171
- return "—"
172
- return f"{100 * num / denom:.0f}%"
173
-
174
-
175
- def per_user_metrics(d: dict, signals: dict) -> dict:
176
- n_sessions = len(d["sessions"]) if signals["session_id"] else None
177
- avg_per_session = (d["invocations"] / n_sessions) if n_sessions else None
178
- single_shot = sum(1 for c in d["session_counts"].values() if c == 1) if n_sessions else None
179
- eff_rate_pct = (
180
- 100 * d["consumed_yes"] / d["consumed_known"]
181
- if signals["output_consumed"] and d["consumed_known"]
182
- else None
183
- )
184
- single_rate_pct = (100 * single_shot / n_sessions) if (n_sessions and single_shot is not None) else None
185
- return {
186
- "invocations": d["invocations"],
187
- "sessions": n_sessions,
188
- "avg_per_session": avg_per_session,
189
- "distinct_tools": len(d["tools"]),
190
- "effectiveness_rate_pct": eff_rate_pct,
191
- "single_shot_rate_pct": single_rate_pct,
192
- }
193
-
194
-
195
- def build_data_caveats_line(signals: dict) -> str:
196
- missing = [k for k, v in signals.items() if not v]
197
- if not missing:
198
- return "**Data signals available:** all optional fields present — full report."
199
- return (
200
- "**Data caveats:** input is missing "
201
- + ", ".join(f"`{m}`" for m in missing)
202
- + " — affected metrics shown as `—`."
203
- )
204
-
205
-
206
- def build_missing_signals_section(signals: dict) -> str:
207
- lines = []
208
- if not signals["session_id"]:
209
- lines.append("- `session_id` absent → **session count, avg/session, and single-shot rate** could not be computed.")
210
- if not signals["output_consumed"]:
211
- lines.append("- `output_consumed` absent → **effectiveness rate** could not be computed.")
212
- if not signals["consumed_at"]:
213
- lines.append("- `consumed_at` absent → **output-to-shipped lag** could not be computed.")
214
- if not signals["outcome"]:
215
- lines.append("- `outcome` absent → cannot filter out errored invocations; raw counts include errors.")
216
- if not lines:
217
- return "All optional signals were present; the report contains all available columns."
218
- return "\n".join(lines)
219
-
220
-
221
- def load_template(name: str, script_dir: Path) -> str:
222
- p = script_dir.parent / "templates" / name
223
- if not p.exists():
224
- raise SystemExit(f"error: template not found: {p}")
225
- return p.read_text(encoding="utf-8")
226
-
227
-
228
- # --------------------------------------------------------------------- views
229
-
230
-
231
- def render_per_user(per_user, tool_counter, signals, ctx, sort_for_effectiveness=False):
232
- template = load_template(
233
- "report-per-user.md" if not sort_for_effectiveness else "report-per-user.md",
234
- ctx["script_dir"],
235
- )
236
-
237
- metrics_by_user = {u: per_user_metrics(d, signals) for u, d in per_user.items()}
238
-
239
- if sort_for_effectiveness:
240
- # Highest single-shot rate first (most theatrical pattern); tie-break by lowest effectiveness
241
- def key(u):
242
- m = metrics_by_user[u]
243
- ss = m["single_shot_rate_pct"] if m["single_shot_rate_pct"] is not None else -1
244
- er = m["effectiveness_rate_pct"] if m["effectiveness_rate_pct"] is not None else 101
245
- return (-ss, er)
246
- users_sorted = sorted(metrics_by_user.keys(), key=key)
247
- else:
248
- users_sorted = sorted(metrics_by_user.keys())
249
-
250
- rows = []
251
- for u in users_sorted:
252
- m = metrics_by_user[u]
253
- sessions_str = str(m["sessions"]) if m["sessions"] is not None else "—"
254
- avg_str = f"{m['avg_per_session']:.1f}" if m["avg_per_session"] is not None else "—"
255
- eff_str = f"{m['effectiveness_rate_pct']:.0f}%" if m["effectiveness_rate_pct"] is not None else "—"
256
- ss_str = f"{m['single_shot_rate_pct']:.0f}%" if m["single_shot_rate_pct"] is not None else "—"
257
- rows.append(
258
- f"| {u} | {m['invocations']} | {sessions_str} | {avg_str} | "
259
- f"{m['distinct_tools']} | {eff_str} | {ss_str} |"
260
- )
261
-
262
- total_invocations = sum(tool_counter.values())
263
- top_tools_rows = "\n".join(
264
- f"| {t} | {c} | {100 * c / total_invocations:.0f}% |"
265
- for t, c in tool_counter.most_common(10)
266
- )
267
-
268
- return template.format(
269
- team_name=ctx["team_name"],
270
- start_date=ctx["start"].date().isoformat(),
271
- end_date=ctx["end"].date().isoformat(),
272
- n_days=ctx["n_days"],
273
- n_users=len(per_user),
274
- n_invocations=total_invocations,
275
- n_distinct_tools=len(tool_counter),
276
- data_caveats_line=ctx["caveats_line"],
277
- per_user_rows="\n".join(rows),
278
- top_tools_rows=top_tools_rows,
279
- missing_signals_section=ctx["missing_section"],
280
- generated_at=ctx["generated_at"],
281
- input_file=ctx["input_file"],
282
- )
283
-
284
-
285
- def render_team_rollup(per_user, tool_counter, signals, ctx):
286
- if len(per_user) < MIN_USERS_FOR_ROLLUP:
287
- raise SystemExit(
288
- f"error: team-rollup requires ≥{MIN_USERS_FOR_ROLLUP} users in scope; "
289
- f"got {len(per_user)}. Aggregate views aren't anonymous at that size. "
290
- f"Use --view per-user and treat the output as identifying."
291
- )
292
-
293
- template = load_template("report-team-rollup.md", ctx["script_dir"])
294
- n_days = max(1, ctx["n_days"])
295
-
296
- invocations_per_user_per_day = [
297
- d["invocations"] / n_days for d in per_user.values()
298
- ]
299
- distinct_tools_per_user = [len(d["tools"]) for d in per_user.values()]
300
-
301
- # Effectiveness (team avg, weighted by known events)
302
- total_known = sum(d["consumed_known"] for d in per_user.values())
303
- total_yes = sum(d["consumed_yes"] for d in per_user.values())
304
- team_eff = (
305
- f"{100 * total_yes / total_known:.0f}%"
306
- if signals["output_consumed"] and total_known
307
- else "—"
308
- )
309
-
310
- # Single-shot (team avg, weighted by sessions)
311
- total_sessions = sum(len(d["sessions"]) for d in per_user.values())
312
- total_single_shot = sum(
313
- sum(1 for c in d["session_counts"].values() if c == 1)
314
- for d in per_user.values()
315
- )
316
- team_ss = (
317
- f"{100 * total_single_shot / total_sessions:.0f}%"
318
- if signals["session_id"] and total_sessions
319
- else "—"
320
- )
321
-
322
- # Engagement tiers (normalized to weekly thresholds)
323
- week_scale = 7 / n_days
324
- tiers = {"heavy": 0, "moderate": 0, "light": 0, "inactive": 0}
325
- for d in per_user.values():
326
- weekly = d["invocations"] * week_scale
327
- if weekly == 0:
328
- tiers["inactive"] += 1
329
- elif weekly > 20:
330
- tiers["heavy"] += 1
331
- elif weekly >= 5:
332
- tiers["moderate"] += 1
333
- else:
334
- tiers["light"] += 1
335
-
336
- n_active = sum(1 for d in per_user.values() if d["invocations"] > 0)
337
-
338
- if signals["output_consumed"] and total_known:
339
- eff_block = (
340
- f"Team-wide, **{100 * total_yes / total_known:.0f}%** of invocations "
341
- f"with a known outcome had their output consumed in a shipped artifact "
342
- f"({total_yes:,} of {total_known:,}). The remaining "
343
- f"{total_known - total_yes:,} invocations produced output that was either "
344
- f"discarded, never reviewed, or not yet tracked as consumed."
345
- )
346
- else:
347
- eff_block = (
348
- "**Effectiveness signal is unavailable** — the input lacks `output_consumed` "
349
- "data. Volume and engagement-tier metrics above are still valid, but the "
350
- "performative-vs-effective distinction cannot be computed from this input."
351
- )
352
-
353
- total_invocations = sum(tool_counter.values())
354
- top_tools_rows = "\n".join(
355
- f"| {t} | {c} | {100 * c / total_invocations:.0f}% |"
356
- for t, c in tool_counter.most_common(10)
357
- )
358
-
359
- return template.format(
360
- team_name=ctx["team_name"],
361
- start_date=ctx["start"].date().isoformat(),
362
- end_date=ctx["end"].date().isoformat(),
363
- n_days=n_days,
364
- n_users=len(per_user),
365
- n_invocations=total_invocations,
366
- data_caveats_line=ctx["caveats_line"],
367
- median_invocations_per_user_per_day=f"{median(invocations_per_user_per_day):.1f}" if invocations_per_user_per_day else "0",
368
- median_distinct_tools_per_user=f"{median(distinct_tools_per_user):.0f}" if distinct_tools_per_user else "0",
369
- team_effectiveness_rate=team_eff,
370
- team_single_shot_rate=team_ss,
371
- n_active_users=n_active,
372
- n_heavy=tiers["heavy"],
373
- n_moderate=tiers["moderate"],
374
- n_light=tiers["light"],
375
- n_inactive=tiers["inactive"],
376
- top_tools_rows=top_tools_rows,
377
- effectiveness_block=eff_block,
378
- missing_signals_section=ctx["missing_section"],
379
- generated_at=ctx["generated_at"],
380
- input_file=ctx["input_file"],
381
- )
382
-
383
-
384
- # --------------------------------------------------------------------- driver
385
-
386
-
387
- def main(argv) -> int:
388
- ap = argparse.ArgumentParser(
389
- description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
390
- )
391
- ap.add_argument("input", help="Path to events file (.csv or .jsonl)")
392
- ap.add_argument("--view", choices=VIEWS, default="per-user")
393
- ap.add_argument("--period", choices=("week", "month"), default="week")
394
- ap.add_argument("--from", dest="from_arg", default=None, help="ISO date — overrides --period")
395
- ap.add_argument("--to", dest="to_arg", default=None, help="ISO date — overrides --period")
396
- ap.add_argument("--team-name", default="Team")
397
- ap.add_argument("-o", "--out", default=None, help="Output markdown path")
398
- args = ap.parse_args(argv)
399
-
400
- input_path = Path(args.input).expanduser().resolve()
401
- if not input_path.exists():
402
- sys.stderr.write(f"error: input file not found: {input_path}\n")
403
- return 1
404
-
405
- events = load_events(input_path)
406
- if not events:
407
- sys.stderr.write("error: input file has no events\n")
408
- return 1
409
- missing = [k for k in ("timestamp", "user", "tool") if not any(k in e for e in events)]
410
- if missing:
411
- sys.stderr.write(f"error: input is missing required field(s): {', '.join(missing)} "
412
- f"(see templates/usage-event-schema.md)\n")
413
- return 1
414
-
415
- signals = detect_signals(events)
416
- start, end = resolve_period(args.period, args.from_arg, args.to_arg)
417
- events = filter_to_period(events, start, end)
418
- if not events:
419
- sys.stderr.write(f"error: no events fell within {start.isoformat()} → {end.isoformat()}\n")
420
- return 1
421
-
422
- per_user, tool_counter = aggregate(events, signals)
423
-
424
- script_dir = Path(__file__).resolve().parent
425
- ctx = {
426
- "team_name": args.team_name,
427
- "start": start,
428
- "end": end,
429
- # a partial day counts, so a date-only --to (pushed to 23:59:59) covers its whole day
430
- "n_days": math.ceil((end - start).total_seconds() / 86400),
431
- "caveats_line": build_data_caveats_line(signals),
432
- "missing_section": build_missing_signals_section(signals),
433
- "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
434
- "input_file": str(input_path.name),
435
- "script_dir": script_dir,
436
- }
437
-
438
- if args.view == "per-user":
439
- out = render_per_user(per_user, tool_counter, signals, ctx)
440
- elif args.view == "effectiveness":
441
- out = render_per_user(per_user, tool_counter, signals, ctx, sort_for_effectiveness=True)
442
- else: # team-rollup
443
- out = render_team_rollup(per_user, tool_counter, signals, ctx)
444
-
445
- out_path = Path(args.out).expanduser().resolve() if args.out else input_path.with_suffix(".report.md")
446
- out_path.parent.mkdir(parents=True, exist_ok=True)
447
- out_path.write_text(out, encoding="utf-8")
448
- print(f"wrote {out_path} ({len(events)} events, {len(per_user)} users, view={args.view})")
449
- return 0
450
-
451
-
452
- if __name__ == "__main__":
453
- sys.exit(main(sys.argv[1:]))
@@ -1,42 +0,0 @@
1
- # AI Usage Report — {team_name}
2
-
3
- **Period:** {start_date} → {end_date} ({n_days} days)
4
- **Users in scope:** {n_users}
5
- **Total invocations:** {n_invocations}
6
- **Distinct tools / skills observed:** {n_distinct_tools}
7
-
8
- > {data_caveats_line}
9
-
10
- ## Per-user breakdown
11
-
12
- | User | Invocations | Sessions | Avg / session | Distinct tools | Effectiveness rate | Single-shot rate |
13
- |---|---:|---:|---:|---:|---:|---:|
14
- {per_user_rows}
15
-
16
- **Reading the table:**
17
- - *Effectiveness rate* — % of this user's invocations whose output was consumed in a shipped artifact. `—` means the input lacked the `output_consumed` field.
18
- - *Single-shot rate* — % of this user's sessions with exactly one invocation. `—` means the input lacked `session_id`.
19
- - *High single-shot + low effectiveness* is a starting place to ask about engagement — not a verdict.
20
-
21
- ## Top tools / skills used (across team)
22
-
23
- | Tool | Invocations | % of total |
24
- |---|---:|---:|
25
- {top_tools_rows}
26
-
27
- ## Where signals are missing
28
-
29
- {missing_signals_section}
30
-
31
- ## How to read this report
32
-
33
- This report shows **patterns**, not verdicts. A user with a low effectiveness rate may be:
34
-
35
- - Exploring (high volume of throwaway prompts while learning a tool)
36
- - Solving in one shot (one prompt is enough — no follow-up needed)
37
- - Doing AI theater (invoking to be seen invoking)
38
-
39
- The data alone cannot distinguish these. Use the report to surface conversations, not to grade individuals.
40
-
41
- ---
42
- *Generated by `ai-usage-report` on {generated_at}. Input file: `{input_file}`. View: `per-user`.*
@@ -1,49 +0,0 @@
1
- # AI Usage Rollup — {team_name}
2
-
3
- **Period:** {start_date} → {end_date} ({n_days} days)
4
- **Team size in scope:** {n_users} users
5
- **Total invocations:** {n_invocations}
6
-
7
- > {data_caveats_line}
8
-
9
- This is an **aggregate** view. No individuals are named. If you need a per-user breakdown for a 1:1 or coaching conversation, switch to the `per-user` view — but read the privacy note at the bottom first.
10
-
11
- ## Headline metrics
12
-
13
- | Metric | Value |
14
- |---|---:|
15
- | Invocations / user / day (median) | {median_invocations_per_user_per_day} |
16
- | Distinct tools per user (median) | {median_distinct_tools_per_user} |
17
- | Effectiveness rate (team avg) | {team_effectiveness_rate} |
18
- | Single-shot session rate (team avg) | {team_single_shot_rate} |
19
- | Active users (≥1 invocation in period) | {n_active_users} of {n_users} |
20
-
21
- ## Adoption distribution
22
-
23
- | Engagement tier | Users | What it means |
24
- |---|---:|---|
25
- | **Heavy** (>20 invocations/week) | {n_heavy} | Deep adopters; check whether their effectiveness rate is also high |
26
- | **Moderate** (5–20 / week) | {n_moderate} | The healthy middle |
27
- | **Light** (1–4 / week) | {n_light} | Sampling but not embedded; surface tool-fit conversations |
28
- | **Inactive** (0 / week) | {n_inactive} | No usage; check whether tool access is the blocker |
29
-
30
- ## Top tools / skills (team-wide)
31
-
32
- | Tool | Invocations | % of total |
33
- |---|---:|---:|
34
- {top_tools_rows}
35
-
36
- ## Effectiveness signal (if available)
37
-
38
- {effectiveness_block}
39
-
40
- ## Where signals are missing
41
-
42
- {missing_signals_section}
43
-
44
- ## Privacy posture
45
-
46
- This view is safe to share with audiences who should not see individual usage data (skip-level meetings, broad team reviews, exec rollups). The headline metrics and tier counts cannot be reverse-engineered to identify individuals **as long as `n_users` ≥ 5**. If your team has fewer than 5 users in scope, the script will refuse to generate this view — switch to `per-user` and treat it accordingly.
47
-
48
- ---
49
- *Generated by `ai-usage-report` on {generated_at}. Input file: `{input_file}`. View: `team-rollup`.*
@@ -1,76 +0,0 @@
1
- # usage-event-schema
2
-
3
- Input to `ai-usage-report` is a CSV or JSONL file. Each row / each JSON object is one **usage event** — a single invocation of an AI tool or skill by a single user.
4
-
5
- ## Required fields
6
-
7
- | Field | Type | Notes |
8
- |---|---|---|
9
- | `timestamp` | ISO 8601 datetime | e.g. `2026-05-21T14:32:11Z` |
10
- | `user` | string | Stable identifier — email, handle, or employee ID. Use the same value for the same person across events. |
11
- | `tool` | string | What was invoked — e.g. `claude-code`, `cursor`, `copilot`, or a specific skill name like `slide-outliner` |
12
-
13
- If any of these three is missing, the script exits with an error. They are the minimum needed to compute a volume report.
14
-
15
- ## Optional but recommended fields
16
-
17
- | Field | Type | Used for |
18
- |---|---|---|
19
- | `session_id` | string | Groups events into sessions. Needed for **session depth** and **single-shot rate**. |
20
- | `output_consumed` | bool / `0` / `1` / `true` / `false` | Whether the AI output ended up in a shipped artifact. Needed for **effectiveness rate**. |
21
- | `consumed_at` | ISO 8601 datetime | When the output landed in the shipped artifact. Needed for **output-to-shipped lag**. |
22
- | `output_size` | int | Tokens or characters of AI output |
23
- | `prompt_size` | int | Tokens or characters of the user prompt (do **not** include the prompt text itself — see privacy note below) |
24
- | `outcome` | enum: `success` / `error` / `cancelled` | Filter for errors that distort volume |
25
-
26
- ## Fields the skill explicitly does NOT want
27
-
28
- - ❌ **The prompt text.** Prompts contain code, customer data, and personal context. The skill works from metadata only.
29
- - ❌ **The output text.** Same reason.
30
- - ❌ **Free-form user notes** about their session. Redact before passing to the skill.
31
-
32
- If your telemetry source captures these, drop them at export time — not after the report is generated.
33
-
34
- ## Graceful degradation
35
-
36
- The script reads what's there and labels missing-signal columns explicitly. The signal-to-field mapping:
37
-
38
- | If your input has… | …the report can show |
39
- |---|---|
40
- | Just the 3 required fields | Volume + breadth |
41
- | `+ session_id` | …and session depth + single-shot rate |
42
- | `+ output_consumed` | …and effectiveness rate + AI-theater flag column |
43
- | `+ consumed_at` | …and output-to-shipped lag |
44
- | `+ outcome` | …and an error-filtered version of every metric |
45
-
46
- When a column is omitted, the report includes a *"Where signals are missing"* section listing exactly which optional fields were absent and which metrics consequently couldn't be computed. This is the contract: the report never lies about what it knows.
47
-
48
- ## Example: CSV
49
-
50
- ```csv
51
- timestamp,user,tool,session_id,output_consumed,consumed_at,outcome
52
- 2026-05-21T09:14:23Z,alice@acme.example,claude-code,sess-001,1,2026-05-21T11:02:00Z,success
53
- 2026-05-21T09:14:55Z,alice@acme.example,claude-code,sess-001,1,2026-05-21T11:02:00Z,success
54
- 2026-05-21T09:18:02Z,alice@acme.example,slide-outliner,sess-001,1,2026-05-21T14:30:00Z,success
55
- 2026-05-21T10:01:11Z,bob@acme.example,cursor,sess-014,0,,success
56
- 2026-05-21T10:01:30Z,bob@acme.example,cursor,sess-014,0,,success
57
- 2026-05-21T14:22:08Z,carol@acme.example,claude-code,sess-029,1,2026-05-22T08:11:00Z,success
58
- ```
59
-
60
- ## Example: JSONL
61
-
62
- ```jsonl
63
- {"timestamp": "2026-05-21T09:14:23Z", "user": "alice@acme.example", "tool": "claude-code", "session_id": "sess-001", "output_consumed": true, "outcome": "success"}
64
- {"timestamp": "2026-05-21T09:14:55Z", "user": "alice@acme.example", "tool": "claude-code", "session_id": "sess-001", "output_consumed": true, "outcome": "success"}
65
- {"timestamp": "2026-05-21T10:01:11Z", "user": "bob@acme.example", "tool": "cursor", "session_id": "sess-014", "output_consumed": false, "outcome": "success"}
66
- ```
67
-
68
- ## A note on the `output_consumed` signal
69
-
70
- This is the field that matters most for the effective-vs-performative distinction. It's also the hardest one to populate accurately. Three common implementations, ranked by signal quality:
71
-
72
- 1. **Diff-based attribution** (best): the telemetry source tracks whether AI output text appears (or fuzzy-matches) in a subsequent commit / PR / doc within N hours. High signal, hard to game.
73
- 2. **Explicit accept/reject events** (good): the IDE emits "user accepted the suggestion" vs "user rejected it." Medium signal — accepting and then immediately deleting still counts as consumed.
74
- 3. **Self-reported** (weakest): the user checks a box "did you use this?" Low signal — captures intent but easy to game and high reporting burden.
75
-
76
- If your telemetry source uses (3), don't pretend the resulting effectiveness rate is more reliable than it is. The report can still be useful for spotting outliers, but the numbers should be read with that grain of salt.