skilldrop-cli 0.2.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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/bin/skilldrop.js +338 -0
  4. package/model-routing.json +147 -0
  5. package/package.json +36 -0
  6. package/packs.json +90 -0
  7. package/skills/accessibility-audit/SKILL.md +74 -0
  8. package/skills/accessibility-audit/examples/login-form.md +76 -0
  9. package/skills/accessibility-audit/manifest.json +11 -0
  10. package/skills/accessibility-audit/reference.md +69 -0
  11. package/skills/adr-generator/SKILL.md +49 -0
  12. package/skills/adr-generator/manifest.json +11 -0
  13. package/skills/adr-generator/templates/madr.md +56 -0
  14. package/skills/adr-generator/templates/nygard.md +19 -0
  15. package/skills/ai-usage-report/SKILL.md +111 -0
  16. package/skills/ai-usage-report/examples/weekly-engineering-team.md +133 -0
  17. package/skills/ai-usage-report/manifest.json +11 -0
  18. package/skills/ai-usage-report/scripts/build_report.py +436 -0
  19. package/skills/ai-usage-report/templates/report-per-user.md +42 -0
  20. package/skills/ai-usage-report/templates/report-team-rollup.md +49 -0
  21. package/skills/ai-usage-report/templates/usage-event-schema.md +76 -0
  22. package/skills/api-contract-draft/SKILL.md +69 -0
  23. package/skills/api-contract-draft/manifest.json +11 -0
  24. package/skills/api-contract-draft/reference.md +73 -0
  25. package/skills/api-contract-draft/templates/openapi-skeleton.yaml +148 -0
  26. package/skills/architecture-diagrams/SKILL.md +66 -0
  27. package/skills/architecture-diagrams/examples/aws-three-tier.md +44 -0
  28. package/skills/architecture-diagrams/examples/c4-container-saas.md +51 -0
  29. package/skills/architecture-diagrams/examples/microservices-sequence.md +49 -0
  30. package/skills/architecture-diagrams/manifest.json +28 -0
  31. package/skills/architecture-diagrams/templates/c4-container.puml +31 -0
  32. package/skills/architecture-diagrams/templates/mermaid-aws.md +52 -0
  33. package/skills/architecture-diagrams/templates/mermaid-flowchart.md +37 -0
  34. package/skills/architecture-diagrams/templates/mermaid-sequence.md +39 -0
  35. package/skills/audience-profile/SKILL.md +73 -0
  36. package/skills/audience-profile/manifest.json +11 -0
  37. package/skills/audience-profile/reference.md +175 -0
  38. package/skills/brief-intake/SKILL.md +78 -0
  39. package/skills/brief-intake/manifest.json +11 -0
  40. package/skills/brief-intake/templates/brief-adr.md +41 -0
  41. package/skills/brief-intake/templates/brief-decision-log.md +35 -0
  42. package/skills/brief-intake/templates/brief-deck.md +43 -0
  43. package/skills/brief-intake/templates/brief-design-doc.md +48 -0
  44. package/skills/brief-intake/templates/brief-exec-summary.md +47 -0
  45. package/skills/brief-intake/templates/brief-generic.md +45 -0
  46. package/skills/brief-intake/templates/brief-runbook.md +44 -0
  47. package/skills/brief-intake/templates/brief-tech-comparison.md +38 -0
  48. package/skills/bug-triage/SKILL.md +67 -0
  49. package/skills/bug-triage/manifest.json +11 -0
  50. package/skills/bug-triage/templates/bug-ticket.md +86 -0
  51. package/skills/business-case/SKILL.md +64 -0
  52. package/skills/business-case/manifest.json +11 -0
  53. package/skills/business-case/templates/business-case.md +60 -0
  54. package/skills/capacity-cost-model/SKILL.md +64 -0
  55. package/skills/capacity-cost-model/manifest.json +11 -0
  56. package/skills/capacity-cost-model/reference.md +54 -0
  57. package/skills/capacity-cost-model/templates/cost-model.md +68 -0
  58. package/skills/council-review/SKILL.md +106 -0
  59. package/skills/council-review/examples/redis-cache-decision.md +75 -0
  60. package/skills/council-review/manifest.json +11 -0
  61. package/skills/council-review/reference.md +76 -0
  62. package/skills/council-review/seats/architect.md +29 -0
  63. package/skills/council-review/seats/bench.md +39 -0
  64. package/skills/council-review/seats/operator.md +27 -0
  65. package/skills/council-review/seats/pragmatist.md +29 -0
  66. package/skills/council-review/seats/security-engineer.md +29 -0
  67. package/skills/council-review/seats/user-advocate.md +27 -0
  68. package/skills/council-review/templates/council-review.md +59 -0
  69. package/skills/data-contract/SKILL.md +67 -0
  70. package/skills/data-contract/manifest.json +11 -0
  71. package/skills/data-contract/reference.md +46 -0
  72. package/skills/data-contract/templates/data-contract.md +69 -0
  73. package/skills/db-schema-design/SKILL.md +68 -0
  74. package/skills/db-schema-design/manifest.json +11 -0
  75. package/skills/db-schema-design/reference.md +56 -0
  76. package/skills/db-schema-design/templates/schema-design.md +75 -0
  77. package/skills/decision-log/SKILL.md +67 -0
  78. package/skills/decision-log/manifest.json +11 -0
  79. package/skills/deck-builder/SKILL.md +114 -0
  80. package/skills/deck-builder/examples/exec-board-update.md +108 -0
  81. package/skills/deck-builder/manifest.json +17 -0
  82. package/skills/deck-builder/reference.md +92 -0
  83. package/skills/deck-builder/requirements.txt +1 -0
  84. package/skills/deck-builder/scripts/build_deck.py +309 -0
  85. package/skills/deck-builder/templates/deck-spec.json +97 -0
  86. package/skills/deck-builder/templates/palettes.json +59 -0
  87. package/skills/design-doc/SKILL.md +49 -0
  88. package/skills/design-doc/manifest.json +11 -0
  89. package/skills/design-doc/templates/design-doc.md +98 -0
  90. package/skills/devils-advocate/SKILL.md +142 -0
  91. package/skills/devils-advocate/lenses/adversarial-review.md +88 -0
  92. package/skills/devils-advocate/lenses/edge-cases.md +95 -0
  93. package/skills/devils-advocate/lenses/future-proofing.md +75 -0
  94. package/skills/devils-advocate/lenses/test-coverage.md +90 -0
  95. package/skills/devils-advocate/manifest.json +11 -0
  96. package/skills/devils-advocate/templates/challenge.md +118 -0
  97. package/skills/doc-critique/SKILL.md +119 -0
  98. package/skills/doc-critique/manifest.json +11 -0
  99. package/skills/doc-critique/rubrics/adr.md +36 -0
  100. package/skills/doc-critique/rubrics/decision-log.md +39 -0
  101. package/skills/doc-critique/rubrics/deck.md +45 -0
  102. package/skills/doc-critique/rubrics/design-doc.md +42 -0
  103. package/skills/doc-critique/rubrics/exec-summary.md +41 -0
  104. package/skills/doc-critique/rubrics/generic.md +40 -0
  105. package/skills/doc-critique/rubrics/runbook.md +42 -0
  106. package/skills/doc-critique/rubrics/tech-comparison.md +40 -0
  107. package/skills/doc-critique/templates/critique.md +49 -0
  108. package/skills/exec-summary/SKILL.md +45 -0
  109. package/skills/exec-summary/manifest.json +11 -0
  110. package/skills/exec-summary/templates/exec-summary.md +48 -0
  111. package/skills/feature-implement-loop/SKILL.md +83 -0
  112. package/skills/feature-implement-loop/manifest.json +11 -0
  113. package/skills/figma-diagrams/SKILL.md +85 -0
  114. package/skills/figma-diagrams/manifest.json +35 -0
  115. package/skills/figma-diagrams/reference.md +91 -0
  116. package/skills/figma-diagrams/requirements.txt +1 -0
  117. package/skills/figma-diagrams/scripts/_figma_client.py +71 -0
  118. package/skills/figma-diagrams/scripts/frame_to_mermaid.py +102 -0
  119. package/skills/figma-diagrams/scripts/inspect_file.py +46 -0
  120. package/skills/figma-diagrams/scripts/list_comments.py +35 -0
  121. package/skills/figma-diagrams/scripts/post_comment.py +52 -0
  122. package/skills/figma-diagrams/templates/figjam-import.json +20 -0
  123. package/skills/guide-builder/SKILL.md +76 -0
  124. package/skills/guide-builder/examples/local-setup-guide.md +127 -0
  125. package/skills/guide-builder/manifest.json +11 -0
  126. package/skills/guide-builder/templates/api-reference.md +120 -0
  127. package/skills/guide-builder/templates/design-walkthrough.md +72 -0
  128. package/skills/guide-builder/templates/setup-guide.md +109 -0
  129. package/skills/incident-comms/SKILL.md +68 -0
  130. package/skills/incident-comms/manifest.json +11 -0
  131. package/skills/incident-comms/reference.md +43 -0
  132. package/skills/incident-comms/templates/messages.md +82 -0
  133. package/skills/llm-eval-harness/SKILL.md +73 -0
  134. package/skills/llm-eval-harness/manifest.json +11 -0
  135. package/skills/llm-eval-harness/reference.md +58 -0
  136. package/skills/llm-eval-harness/templates/eval-plan.md +63 -0
  137. package/skills/llm-eval-harness/templates/golden-case.jsonl +5 -0
  138. package/skills/migration-plan/SKILL.md +68 -0
  139. package/skills/migration-plan/manifest.json +11 -0
  140. package/skills/migration-plan/templates/migration-plan.md +68 -0
  141. package/skills/nfr-spec/SKILL.md +63 -0
  142. package/skills/nfr-spec/manifest.json +11 -0
  143. package/skills/nfr-spec/reference.md +74 -0
  144. package/skills/nfr-spec/templates/nfr-spec.md +56 -0
  145. package/skills/observability-plan/SKILL.md +70 -0
  146. package/skills/observability-plan/manifest.json +11 -0
  147. package/skills/observability-plan/reference.md +67 -0
  148. package/skills/observability-plan/templates/observability-plan.md +62 -0
  149. package/skills/okr-cascade/SKILL.md +59 -0
  150. package/skills/okr-cascade/evals/eval_queries.json +12 -0
  151. package/skills/okr-cascade/evals/evals.json +28 -0
  152. package/skills/okr-cascade/manifest.json +11 -0
  153. package/skills/okr-cascade/templates/okr-cascade.md +46 -0
  154. package/skills/postmortem-generator/SKILL.md +65 -0
  155. package/skills/postmortem-generator/manifest.json +11 -0
  156. package/skills/postmortem-generator/templates/postmortem.md +67 -0
  157. package/skills/prd-draft/SKILL.md +62 -0
  158. package/skills/prd-draft/manifest.json +11 -0
  159. package/skills/prd-draft/templates/prd.md +66 -0
  160. package/skills/prfaq/SKILL.md +75 -0
  161. package/skills/prfaq/evals/eval_queries.json +13 -0
  162. package/skills/prfaq/evals/evals.json +32 -0
  163. package/skills/prfaq/manifest.json +11 -0
  164. package/skills/prfaq/templates/prfaq.md +60 -0
  165. package/skills/release-notes/SKILL.md +61 -0
  166. package/skills/release-notes/manifest.json +11 -0
  167. package/skills/release-notes/templates/release-notes.md +83 -0
  168. package/skills/requirements-interview/SKILL.md +61 -0
  169. package/skills/requirements-interview/manifest.json +11 -0
  170. package/skills/requirements-interview/templates/interview-kit.md +63 -0
  171. package/skills/reverse-architecture/SKILL.md +103 -0
  172. package/skills/reverse-architecture/manifest.json +11 -0
  173. package/skills/reverse-architecture/reference.md +150 -0
  174. package/skills/reverse-architecture/templates/description.md +52 -0
  175. package/skills/reverse-architecture/templates/extraction.md +55 -0
  176. package/skills/runbook-generator/SKILL.md +42 -0
  177. package/skills/runbook-generator/manifest.json +11 -0
  178. package/skills/runbook-generator/templates/runbook.md +109 -0
  179. package/skills/slide-outliner/SKILL.md +58 -0
  180. package/skills/slide-outliner/manifest.json +11 -0
  181. package/skills/slide-outliner/templates/deck-outline.md +131 -0
  182. package/skills/sonar-onboard/SKILL.md +170 -0
  183. package/skills/sonar-onboard/manifest.json +11 -0
  184. package/skills/sonar-onboard/reference.md +151 -0
  185. package/skills/sonar-onboard/templates/github-actions-sonar-server.yml.tmpl +50 -0
  186. package/skills/sonar-onboard/templates/github-actions-sonarcloud.yml.tmpl +53 -0
  187. package/skills/sonar-onboard/templates/readme-snippet.md +38 -0
  188. package/skills/sonar-onboard/templates/sonar-project.properties.tmpl +31 -0
  189. package/skills/sonar-review/SKILL.md +220 -0
  190. package/skills/sonar-review/lenses/bugs.md +48 -0
  191. package/skills/sonar-review/lenses/code-smells.md +64 -0
  192. package/skills/sonar-review/lenses/coverage-and-duplication.md +74 -0
  193. package/skills/sonar-review/lenses/security-hotspots.md +56 -0
  194. package/skills/sonar-review/lenses/vulnerabilities.md +52 -0
  195. package/skills/sonar-review/manifest.json +37 -0
  196. package/skills/sonar-review/reference.md +167 -0
  197. package/skills/sonar-review/templates/report.md +84 -0
  198. package/skills/strategy-analysis/SKILL.md +63 -0
  199. package/skills/strategy-analysis/evals/eval_queries.json +13 -0
  200. package/skills/strategy-analysis/evals/evals.json +36 -0
  201. package/skills/strategy-analysis/manifest.json +11 -0
  202. package/skills/success-metrics/SKILL.md +63 -0
  203. package/skills/success-metrics/evals/eval_queries.json +11 -0
  204. package/skills/success-metrics/evals/evals.json +27 -0
  205. package/skills/success-metrics/manifest.json +11 -0
  206. package/skills/success-metrics/templates/metrics-plan.md +55 -0
  207. package/skills/tech-comparison-matrix/SKILL.md +56 -0
  208. package/skills/tech-comparison-matrix/manifest.json +11 -0
  209. package/skills/tech-comparison-matrix/templates/matrix.md +45 -0
  210. package/skills/test-plan-generator/SKILL.md +59 -0
  211. package/skills/test-plan-generator/manifest.json +11 -0
  212. package/skills/test-plan-generator/templates/test-plan.md +83 -0
  213. package/skills/threat-model/SKILL.md +65 -0
  214. package/skills/threat-model/examples/document-upload.md +56 -0
  215. package/skills/threat-model/manifest.json +11 -0
  216. package/skills/threat-model/reference.md +69 -0
  217. package/skills/user-journey-map/SKILL.md +64 -0
  218. package/skills/user-journey-map/evals/eval_queries.json +12 -0
  219. package/skills/user-journey-map/evals/evals.json +30 -0
  220. package/skills/user-journey-map/examples/b2b-saas-onboarding.md +69 -0
  221. package/skills/user-journey-map/manifest.json +11 -0
  222. package/skills/user-journey-map/templates/journey-map.md +47 -0
  223. package/skills/user-story-splitter/SKILL.md +88 -0
  224. package/skills/user-story-splitter/examples/notifications-epic.md +123 -0
  225. package/skills/user-story-splitter/manifest.json +11 -0
  226. package/skills/user-story-splitter/templates/story.md +40 -0
@@ -0,0 +1,436 @@
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 sys
19
+ from collections import Counter, defaultdict
20
+ from datetime import datetime, timedelta, timezone
21
+ from pathlib import Path
22
+ from statistics import median
23
+
24
+ VIEWS = ("per-user", "team-rollup", "effectiveness")
25
+ MIN_USERS_FOR_ROLLUP = 5
26
+
27
+
28
+ def load_events(path: Path) -> list[dict]:
29
+ suffix = path.suffix.lower()
30
+ if suffix == ".csv":
31
+ with path.open(newline="") as f:
32
+ return list(csv.DictReader(f))
33
+ if suffix in (".jsonl", ".ndjson"):
34
+ events = []
35
+ with path.open() as f:
36
+ for line in f:
37
+ s = line.strip()
38
+ if s:
39
+ events.append(json.loads(s))
40
+ return events
41
+ raise SystemExit(f"error: unsupported file type {suffix!r} (use .csv or .jsonl)")
42
+
43
+
44
+ def parse_ts(s) -> datetime | None:
45
+ if not s:
46
+ return None
47
+ if isinstance(s, datetime):
48
+ return s if s.tzinfo else s.replace(tzinfo=timezone.utc)
49
+ s = str(s).strip()
50
+ if not s:
51
+ return None
52
+ if s.endswith("Z"):
53
+ s = s[:-1] + "+00:00"
54
+ try:
55
+ dt = datetime.fromisoformat(s)
56
+ except ValueError:
57
+ # Try date-only
58
+ try:
59
+ dt = datetime.fromisoformat(s + "T00:00:00+00:00")
60
+ except ValueError:
61
+ return None
62
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
63
+
64
+
65
+ def parse_bool(v):
66
+ if v is None or v == "":
67
+ return None
68
+ if isinstance(v, bool):
69
+ return v
70
+ s = str(v).strip().lower()
71
+ if s in ("1", "true", "yes", "y", "t"):
72
+ return True
73
+ if s in ("0", "false", "no", "n", "f"):
74
+ return False
75
+ return None
76
+
77
+
78
+ def resolve_period(period: str, from_arg: str | None, to_arg: str | None):
79
+ if from_arg or to_arg:
80
+ if not (from_arg and to_arg):
81
+ raise SystemExit("error: --from and --to must be used together")
82
+ start = parse_ts(from_arg)
83
+ end = parse_ts(to_arg)
84
+ if start is None or end is None:
85
+ raise SystemExit(f"error: could not parse --from / --to dates")
86
+ # If user passed date-only for --to, push to end of day
87
+ if len(to_arg) <= 10:
88
+ end = end + timedelta(days=1, seconds=-1)
89
+ return start, end
90
+ now = datetime.now(timezone.utc)
91
+ if period == "week":
92
+ return now - timedelta(days=7), now
93
+ if period == "month":
94
+ return now - timedelta(days=30), now
95
+ raise SystemExit(f"error: unknown --period {period!r} (use week, month, or --from/--to)")
96
+
97
+
98
+ def detect_signals(events: list[dict]) -> dict:
99
+ """Which optional fields are populated in the input."""
100
+ keys_present = set()
101
+ for e in events[:1000]: # sample
102
+ for k, v in e.items():
103
+ if v not in (None, ""):
104
+ keys_present.add(k)
105
+ return {
106
+ "session_id": "session_id" in keys_present,
107
+ "output_consumed": "output_consumed" in keys_present,
108
+ "consumed_at": "consumed_at" in keys_present,
109
+ "outcome": "outcome" in keys_present,
110
+ }
111
+
112
+
113
+ def filter_to_period(events: list[dict], start: datetime, end: datetime) -> list[dict]:
114
+ out = []
115
+ for e in events:
116
+ ts = parse_ts(e.get("timestamp"))
117
+ if ts and start <= ts <= end:
118
+ out.append({**e, "_ts": ts})
119
+ return out
120
+
121
+
122
+ def aggregate(events: list[dict], signals: dict):
123
+ per_user = defaultdict(lambda: {
124
+ "invocations": 0,
125
+ "sessions": set(),
126
+ "tools": set(),
127
+ "session_counts": defaultdict(int),
128
+ "consumed_known": 0,
129
+ "consumed_yes": 0,
130
+ })
131
+ tool_counter: Counter = Counter()
132
+
133
+ for e in events:
134
+ u = e.get("user")
135
+ if not u:
136
+ continue
137
+ d = per_user[u]
138
+ d["invocations"] += 1
139
+ tool = e.get("tool") or "(unknown)"
140
+ d["tools"].add(tool)
141
+ tool_counter[tool] += 1
142
+
143
+ if signals["session_id"]:
144
+ sid = e.get("session_id")
145
+ if sid:
146
+ d["sessions"].add(sid)
147
+ d["session_counts"][sid] += 1
148
+
149
+ if signals["output_consumed"]:
150
+ b = parse_bool(e.get("output_consumed"))
151
+ if b is not None:
152
+ d["consumed_known"] += 1
153
+ if b:
154
+ d["consumed_yes"] += 1
155
+
156
+ return per_user, tool_counter
157
+
158
+
159
+ def fmt_pct(num, denom) -> str:
160
+ if not denom:
161
+ return "—"
162
+ return f"{100 * num / denom:.0f}%"
163
+
164
+
165
+ def per_user_metrics(d: dict, signals: dict) -> dict:
166
+ n_sessions = len(d["sessions"]) if signals["session_id"] else None
167
+ avg_per_session = (d["invocations"] / n_sessions) if n_sessions else None
168
+ single_shot = sum(1 for c in d["session_counts"].values() if c == 1) if n_sessions else None
169
+ eff_rate_pct = (
170
+ 100 * d["consumed_yes"] / d["consumed_known"]
171
+ if signals["output_consumed"] and d["consumed_known"]
172
+ else None
173
+ )
174
+ single_rate_pct = (100 * single_shot / n_sessions) if (n_sessions and single_shot is not None) else None
175
+ return {
176
+ "invocations": d["invocations"],
177
+ "sessions": n_sessions,
178
+ "avg_per_session": avg_per_session,
179
+ "distinct_tools": len(d["tools"]),
180
+ "effectiveness_rate_pct": eff_rate_pct,
181
+ "single_shot_rate_pct": single_rate_pct,
182
+ }
183
+
184
+
185
+ def build_data_caveats_line(signals: dict) -> str:
186
+ missing = [k for k, v in signals.items() if not v]
187
+ if not missing:
188
+ return "**Data signals available:** all optional fields present — full report."
189
+ return (
190
+ "**Data caveats:** input is missing "
191
+ + ", ".join(f"`{m}`" for m in missing)
192
+ + " — affected metrics shown as `—`."
193
+ )
194
+
195
+
196
+ def build_missing_signals_section(signals: dict) -> str:
197
+ lines = []
198
+ if not signals["session_id"]:
199
+ lines.append("- `session_id` absent → **session count, avg/session, and single-shot rate** could not be computed.")
200
+ if not signals["output_consumed"]:
201
+ lines.append("- `output_consumed` absent → **effectiveness rate** could not be computed.")
202
+ if not signals["consumed_at"]:
203
+ lines.append("- `consumed_at` absent → **output-to-shipped lag** could not be computed.")
204
+ if not signals["outcome"]:
205
+ lines.append("- `outcome` absent → cannot filter out errored invocations; raw counts include errors.")
206
+ if not lines:
207
+ return "All optional signals were present; the report contains all available columns."
208
+ return "\n".join(lines)
209
+
210
+
211
+ def load_template(name: str, script_dir: Path) -> str:
212
+ p = script_dir.parent / "templates" / name
213
+ if not p.exists():
214
+ raise SystemExit(f"error: template not found: {p}")
215
+ return p.read_text()
216
+
217
+
218
+ # --------------------------------------------------------------------- views
219
+
220
+
221
+ def render_per_user(per_user, tool_counter, signals, ctx, sort_for_effectiveness=False):
222
+ template = load_template(
223
+ "report-per-user.md" if not sort_for_effectiveness else "report-per-user.md",
224
+ ctx["script_dir"],
225
+ )
226
+
227
+ metrics_by_user = {u: per_user_metrics(d, signals) for u, d in per_user.items()}
228
+
229
+ if sort_for_effectiveness:
230
+ # Highest single-shot rate first (most theatrical pattern); tie-break by lowest effectiveness
231
+ def key(u):
232
+ m = metrics_by_user[u]
233
+ ss = m["single_shot_rate_pct"] if m["single_shot_rate_pct"] is not None else -1
234
+ er = m["effectiveness_rate_pct"] if m["effectiveness_rate_pct"] is not None else 101
235
+ return (-ss, er)
236
+ users_sorted = sorted(metrics_by_user.keys(), key=key)
237
+ else:
238
+ users_sorted = sorted(metrics_by_user.keys())
239
+
240
+ rows = []
241
+ for u in users_sorted:
242
+ m = metrics_by_user[u]
243
+ sessions_str = str(m["sessions"]) if m["sessions"] is not None else "—"
244
+ avg_str = f"{m['avg_per_session']:.1f}" if m["avg_per_session"] is not None else "—"
245
+ eff_str = f"{m['effectiveness_rate_pct']:.0f}%" if m["effectiveness_rate_pct"] is not None else "—"
246
+ ss_str = f"{m['single_shot_rate_pct']:.0f}%" if m["single_shot_rate_pct"] is not None else "—"
247
+ rows.append(
248
+ f"| {u} | {m['invocations']} | {sessions_str} | {avg_str} | "
249
+ f"{m['distinct_tools']} | {eff_str} | {ss_str} |"
250
+ )
251
+
252
+ total_invocations = sum(tool_counter.values())
253
+ top_tools_rows = "\n".join(
254
+ f"| {t} | {c} | {100 * c / total_invocations:.0f}% |"
255
+ for t, c in tool_counter.most_common(10)
256
+ )
257
+
258
+ return template.format(
259
+ team_name=ctx["team_name"],
260
+ start_date=ctx["start"].date().isoformat(),
261
+ end_date=ctx["end"].date().isoformat(),
262
+ n_days=(ctx["end"] - ctx["start"]).days,
263
+ n_users=len(per_user),
264
+ n_invocations=total_invocations,
265
+ n_distinct_tools=len(tool_counter),
266
+ data_caveats_line=ctx["caveats_line"],
267
+ per_user_rows="\n".join(rows),
268
+ top_tools_rows=top_tools_rows,
269
+ missing_signals_section=ctx["missing_section"],
270
+ generated_at=ctx["generated_at"],
271
+ input_file=ctx["input_file"],
272
+ )
273
+
274
+
275
+ def render_team_rollup(per_user, tool_counter, signals, ctx):
276
+ if len(per_user) < MIN_USERS_FOR_ROLLUP:
277
+ raise SystemExit(
278
+ f"error: team-rollup requires ≥{MIN_USERS_FOR_ROLLUP} users in scope; "
279
+ f"got {len(per_user)}. Aggregate views aren't anonymous at that size. "
280
+ f"Use --view per-user and treat the output as identifying."
281
+ )
282
+
283
+ template = load_template("report-team-rollup.md", ctx["script_dir"])
284
+ n_days = max(1, (ctx["end"] - ctx["start"]).days)
285
+
286
+ invocations_per_user_per_day = [
287
+ d["invocations"] / n_days for d in per_user.values()
288
+ ]
289
+ distinct_tools_per_user = [len(d["tools"]) for d in per_user.values()]
290
+
291
+ # Effectiveness (team avg, weighted by known events)
292
+ total_known = sum(d["consumed_known"] for d in per_user.values())
293
+ total_yes = sum(d["consumed_yes"] for d in per_user.values())
294
+ team_eff = (
295
+ f"{100 * total_yes / total_known:.0f}%"
296
+ if signals["output_consumed"] and total_known
297
+ else "—"
298
+ )
299
+
300
+ # Single-shot (team avg, weighted by sessions)
301
+ total_sessions = sum(len(d["sessions"]) for d in per_user.values())
302
+ total_single_shot = sum(
303
+ sum(1 for c in d["session_counts"].values() if c == 1)
304
+ for d in per_user.values()
305
+ )
306
+ team_ss = (
307
+ f"{100 * total_single_shot / total_sessions:.0f}%"
308
+ if signals["session_id"] and total_sessions
309
+ else "—"
310
+ )
311
+
312
+ # Engagement tiers (normalized to weekly thresholds)
313
+ week_scale = 7 / n_days
314
+ tiers = {"heavy": 0, "moderate": 0, "light": 0, "inactive": 0}
315
+ for d in per_user.values():
316
+ weekly = d["invocations"] * week_scale
317
+ if weekly == 0:
318
+ tiers["inactive"] += 1
319
+ elif weekly > 20:
320
+ tiers["heavy"] += 1
321
+ elif weekly >= 5:
322
+ tiers["moderate"] += 1
323
+ else:
324
+ tiers["light"] += 1
325
+
326
+ n_active = sum(1 for d in per_user.values() if d["invocations"] > 0)
327
+
328
+ if signals["output_consumed"] and total_known:
329
+ eff_block = (
330
+ f"Team-wide, **{100 * total_yes / total_known:.0f}%** of invocations "
331
+ f"with a known outcome had their output consumed in a shipped artifact "
332
+ f"({total_yes:,} of {total_known:,}). The remaining "
333
+ f"{total_known - total_yes:,} invocations produced output that was either "
334
+ f"discarded, never reviewed, or not yet tracked as consumed."
335
+ )
336
+ else:
337
+ eff_block = (
338
+ "**Effectiveness signal is unavailable** — the input lacks `output_consumed` "
339
+ "data. Volume and engagement-tier metrics above are still valid, but the "
340
+ "performative-vs-effective distinction cannot be computed from this input."
341
+ )
342
+
343
+ total_invocations = sum(tool_counter.values())
344
+ top_tools_rows = "\n".join(
345
+ f"| {t} | {c} | {100 * c / total_invocations:.0f}% |"
346
+ for t, c in tool_counter.most_common(10)
347
+ )
348
+
349
+ return template.format(
350
+ team_name=ctx["team_name"],
351
+ start_date=ctx["start"].date().isoformat(),
352
+ end_date=ctx["end"].date().isoformat(),
353
+ n_days=n_days,
354
+ n_users=len(per_user),
355
+ n_invocations=total_invocations,
356
+ data_caveats_line=ctx["caveats_line"],
357
+ median_invocations_per_user_per_day=f"{median(invocations_per_user_per_day):.1f}" if invocations_per_user_per_day else "0",
358
+ median_distinct_tools_per_user=f"{median(distinct_tools_per_user):.0f}" if distinct_tools_per_user else "0",
359
+ team_effectiveness_rate=team_eff,
360
+ team_single_shot_rate=team_ss,
361
+ n_active_users=n_active,
362
+ n_heavy=tiers["heavy"],
363
+ n_moderate=tiers["moderate"],
364
+ n_light=tiers["light"],
365
+ n_inactive=tiers["inactive"],
366
+ top_tools_rows=top_tools_rows,
367
+ effectiveness_block=eff_block,
368
+ missing_signals_section=ctx["missing_section"],
369
+ generated_at=ctx["generated_at"],
370
+ input_file=ctx["input_file"],
371
+ )
372
+
373
+
374
+ # --------------------------------------------------------------------- driver
375
+
376
+
377
+ def main(argv) -> int:
378
+ ap = argparse.ArgumentParser(
379
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
380
+ )
381
+ ap.add_argument("input", help="Path to events file (.csv or .jsonl)")
382
+ ap.add_argument("--view", choices=VIEWS, default="per-user")
383
+ ap.add_argument("--period", choices=("week", "month"), default="week")
384
+ ap.add_argument("--from", dest="from_arg", default=None, help="ISO date — overrides --period")
385
+ ap.add_argument("--to", dest="to_arg", default=None, help="ISO date — overrides --period")
386
+ ap.add_argument("--team-name", default="Team")
387
+ ap.add_argument("-o", "--out", default=None, help="Output markdown path")
388
+ args = ap.parse_args(argv)
389
+
390
+ input_path = Path(args.input).expanduser().resolve()
391
+ if not input_path.exists():
392
+ sys.stderr.write(f"error: input file not found: {input_path}\n")
393
+ return 1
394
+
395
+ events = load_events(input_path)
396
+ if not events:
397
+ sys.stderr.write("error: input file has no events\n")
398
+ return 1
399
+
400
+ signals = detect_signals(events)
401
+ start, end = resolve_period(args.period, args.from_arg, args.to_arg)
402
+ events = filter_to_period(events, start, end)
403
+ if not events:
404
+ sys.stderr.write(f"error: no events fell within {start.isoformat()} → {end.isoformat()}\n")
405
+ return 1
406
+
407
+ per_user, tool_counter = aggregate(events, signals)
408
+
409
+ script_dir = Path(__file__).resolve().parent
410
+ ctx = {
411
+ "team_name": args.team_name,
412
+ "start": start,
413
+ "end": end,
414
+ "caveats_line": build_data_caveats_line(signals),
415
+ "missing_section": build_missing_signals_section(signals),
416
+ "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
417
+ "input_file": str(input_path.name),
418
+ "script_dir": script_dir,
419
+ }
420
+
421
+ if args.view == "per-user":
422
+ out = render_per_user(per_user, tool_counter, signals, ctx)
423
+ elif args.view == "effectiveness":
424
+ out = render_per_user(per_user, tool_counter, signals, ctx, sort_for_effectiveness=True)
425
+ else: # team-rollup
426
+ out = render_team_rollup(per_user, tool_counter, signals, ctx)
427
+
428
+ out_path = Path(args.out).expanduser().resolve() if args.out else input_path.with_suffix(".report.md")
429
+ out_path.parent.mkdir(parents=True, exist_ok=True)
430
+ out_path.write_text(out)
431
+ print(f"wrote {out_path} ({len(events)} events, {len(per_user)} users, view={args.view})")
432
+ return 0
433
+
434
+
435
+ if __name__ == "__main__":
436
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,42 @@
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`.*
@@ -0,0 +1,49 @@
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`.*
@@ -0,0 +1,76 @@
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 `deck-builder` |
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.com,claude-code,sess-001,1,2026-05-21T11:02:00Z,success
53
+ 2026-05-21T09:14:55Z,alice@acme.com,claude-code,sess-001,1,2026-05-21T11:02:00Z,success
54
+ 2026-05-21T09:18:02Z,alice@acme.com,deck-builder,sess-001,1,2026-05-21T14:30:00Z,success
55
+ 2026-05-21T10:01:11Z,bob@acme.com,cursor,sess-014,0,,success
56
+ 2026-05-21T10:01:30Z,bob@acme.com,cursor,sess-014,0,,success
57
+ 2026-05-21T14:22:08Z,carol@acme.com,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.com", "tool": "claude-code", "session_id": "sess-001", "output_consumed": true, "outcome": "success"}
64
+ {"timestamp": "2026-05-21T09:14:55Z", "user": "alice@acme.com", "tool": "claude-code", "session_id": "sess-001", "output_consumed": true, "outcome": "success"}
65
+ {"timestamp": "2026-05-21T10:01:11Z", "user": "bob@acme.com", "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.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: api-contract-draft
3
+ description: Draft an OpenAPI 3.1 contract from a feature brief or story — resource-oriented paths, a full error catalog (RFC 9457), pagination/idempotency/versioning decided not deferred, examples on every operation, plus a decisions log so review argues choices instead of YAML. Use when the user wants to design an API, draft an OpenAPI/Swagger spec, define request/response shapes, or agree a contract between frontend/backend or two teams before code.
4
+ ---
5
+
6
+ # api-contract-draft
7
+
8
+ Produces the contract *before* the code — a valid OpenAPI 3.1 document two teams can argue about and then build against in parallel. The YAML is half the artifact; the other half is the **decisions log**, because reviewers should debate "cursor vs offset pagination", not line 240. Designs new contracts; documenting an existing API is `guide-builder`'s job.
9
+
10
+ ## How to respond
11
+
12
+ 1. **Extract the resources from the brief.** Nouns the API owns, their relationships, and their lifecycle (who creates, what mutates, is delete real or soft). Ask at most 2 questions, and spend them on consumer shape ("who calls this — our SPA, partners, both?") and write semantics ("can a refund be requested twice?") — the two answers that change the contract most. Everything else: pick the convention from [`reference.md`](reference.md) and tag the choice `[assumption]` in the decisions log.
13
+
14
+ 2. **Apply the house conventions** (full detail in [`reference.md`](reference.md)) — deviate only when the brief forces it, and log the deviation:
15
+ - **Paths**: plural-noun resources, no verbs — ✅ `POST /refunds` ❌ `POST /createRefund`; nesting one level max, then flatten with filters
16
+ - **Errors**: RFC 9457 `application/problem+json`, every 4xx the operation can return enumerated with a stable machine-readable `code`; no bare unexplained 500
17
+ - **Pagination**: cursor-based by default (`cursor` + `limit`, response carries `next_cursor`); offset only with a logged reason
18
+ - **Idempotency**: every unsafe POST takes `Idempotency-Key`; PUT/DELETE idempotent by construction
19
+ - **Versioning**: URL major version (`/v1/`), additive changes don't bump, breaking changes do — the policy sentence goes in the spec's `description`
20
+ - **Auth**: a `securityScheme` exists and every operation declares its requirement — including the explicitly-public ones
21
+
22
+ 3. **Type every field like a reviewer will reject sloppiness.** Formats (`uuid`, `date-time`, `int64`), constraints (`maxLength`, `minimum`, `enum`, `pattern` for codes), required vs optional explicit on every schema, money as integer minor units (never float), timestamps UTC ISO-8601. A schema that's all bare `type: string` is a draft of a draft.
23
+
24
+ 4. **Give every operation a realistic example** — request and the 2xx response, with values that look like production (`"cur_9f3k..."`, not `"string"`). Examples are where shape disagreements surface; they're the cheapest review tool in the document.
25
+
26
+ 5. **Emit three sections in one message**:
27
+ 1. **Decisions log** — table of every contract decision (resource naming, pagination, error model, versioning, auth, the `[assumption]`s) with a one-line rationale each
28
+ 2. **The OpenAPI 3.1 YAML** — complete and valid: `info` (with the versioning policy), `servers`, `components.securitySchemes`, `components.schemas` (including the `Problem` schema), paths with examples
29
+ 3. **Open questions** — the unknowns that block freezing the contract, each with who can answer it
30
+ Start from [`templates/openapi-skeleton.yaml`](templates/openapi-skeleton.yaml).
31
+
32
+ 6. **For event/queue contracts** (the brief says "publish", "consume", "webhook out"): same discipline, AsyncAPI-shaped — channel name, payload schema with the same typing rigor, delivery semantics stated (at-least-once is the default; say it), and the consumer-retry/DLQ expectation in the channel description.
33
+
34
+ ## Useful references in this skill
35
+
36
+ - [`reference.md`](reference.md) — the house conventions: naming, errors, pagination, idempotency, versioning, field-typing rules
37
+ - [`templates/openapi-skeleton.yaml`](templates/openapi-skeleton.yaml) — valid 3.1 starting point with `Problem` schema, security scheme, and one fully-worked resource
38
+
39
+ ## Quality bar
40
+
41
+ - **The YAML validates.** OpenAPI 3.1, parseable, refs resolve. A contract that doesn't lint is a sketch.
42
+ - **Every operation enumerates its 4xx set** with stable `code` values a client can switch on. "400: Bad Request" alone is an unwritten error contract.
43
+ - **Every decision in the log has a rationale**, and every deviation from `reference.md` conventions is logged as a decision — silent deviation is how two services end up with three pagination styles.
44
+ - **Examples look like production data.** `"id": "string"` in an example means nobody pictured a real call.
45
+ - **No float money, no naked timestamps, no stringly-typed enums.** The three classic contract regrets, all preventable at draft time.
46
+ - **Unsafe operations state their idempotency story.** A reviewer can answer "what happens on retry?" for every POST.
47
+
48
+ ## When to use this skill
49
+
50
+ - ✅ Designing a new API or new resource before implementation
51
+ - ✅ Frontend and backend (or two teams) need a contract to build against in parallel
52
+ - ✅ A story from `user-story-splitter` whose slice is "expose X over the API"
53
+ - ✅ Drafting the webhook/event contract a partner will consume
54
+
55
+ ## When NOT to use this skill
56
+
57
+ - ❌ Documenting an API that already exists — that's `guide-builder` (reference archetype)
58
+ - ❌ Choosing REST vs GraphQL vs gRPC — that's `tech-comparison-matrix`; this skill starts after
59
+ - ❌ Reviewing a contract someone else drafted — `doc-critique` or `council-review`
60
+ - ❌ Generating server/client code from the spec — hand the frozen contract to the codegen tool
61
+
62
+ ## Anti-patterns to avoid
63
+
64
+ - ❌ **RPC verbs in paths** (`/getUser`, `/doRefund`). The resource model is the contract's spine; verbs mean it was never designed.
65
+ - ❌ **The 200-with-error-body.** `200 {"success": false}` breaks every client's error handling; failures use failure status codes.
66
+ - ❌ **Deferring pagination "until we need it".** Retrofitting pagination onto a shipped list endpoint is a breaking change; decide it at draft.
67
+ - ❌ **One giant shared schema** reused for create-request, update-request, and response, with half the fields "ignored on input". Separate the shapes; optionality differs.
68
+ - ❌ **YAML without the decisions log.** Reviewers nitpick property names and never see that offset pagination or float money slipped through.
69
+ - ❌ **Contract churn after parallel build starts.** Once both sides build, every change is a versioned change — the open-questions section exists to drain *before* freezing, not after.