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.
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/bin/skilldrop.js +338 -0
- package/model-routing.json +147 -0
- package/package.json +36 -0
- package/packs.json +90 -0
- package/skills/accessibility-audit/SKILL.md +74 -0
- package/skills/accessibility-audit/examples/login-form.md +76 -0
- package/skills/accessibility-audit/manifest.json +11 -0
- package/skills/accessibility-audit/reference.md +69 -0
- package/skills/adr-generator/SKILL.md +49 -0
- package/skills/adr-generator/manifest.json +11 -0
- package/skills/adr-generator/templates/madr.md +56 -0
- package/skills/adr-generator/templates/nygard.md +19 -0
- package/skills/ai-usage-report/SKILL.md +111 -0
- package/skills/ai-usage-report/examples/weekly-engineering-team.md +133 -0
- package/skills/ai-usage-report/manifest.json +11 -0
- package/skills/ai-usage-report/scripts/build_report.py +436 -0
- package/skills/ai-usage-report/templates/report-per-user.md +42 -0
- package/skills/ai-usage-report/templates/report-team-rollup.md +49 -0
- package/skills/ai-usage-report/templates/usage-event-schema.md +76 -0
- package/skills/api-contract-draft/SKILL.md +69 -0
- package/skills/api-contract-draft/manifest.json +11 -0
- package/skills/api-contract-draft/reference.md +73 -0
- package/skills/api-contract-draft/templates/openapi-skeleton.yaml +148 -0
- package/skills/architecture-diagrams/SKILL.md +66 -0
- package/skills/architecture-diagrams/examples/aws-three-tier.md +44 -0
- package/skills/architecture-diagrams/examples/c4-container-saas.md +51 -0
- package/skills/architecture-diagrams/examples/microservices-sequence.md +49 -0
- package/skills/architecture-diagrams/manifest.json +28 -0
- package/skills/architecture-diagrams/templates/c4-container.puml +31 -0
- package/skills/architecture-diagrams/templates/mermaid-aws.md +52 -0
- package/skills/architecture-diagrams/templates/mermaid-flowchart.md +37 -0
- package/skills/architecture-diagrams/templates/mermaid-sequence.md +39 -0
- package/skills/audience-profile/SKILL.md +73 -0
- package/skills/audience-profile/manifest.json +11 -0
- package/skills/audience-profile/reference.md +175 -0
- package/skills/brief-intake/SKILL.md +78 -0
- package/skills/brief-intake/manifest.json +11 -0
- package/skills/brief-intake/templates/brief-adr.md +41 -0
- package/skills/brief-intake/templates/brief-decision-log.md +35 -0
- package/skills/brief-intake/templates/brief-deck.md +43 -0
- package/skills/brief-intake/templates/brief-design-doc.md +48 -0
- package/skills/brief-intake/templates/brief-exec-summary.md +47 -0
- package/skills/brief-intake/templates/brief-generic.md +45 -0
- package/skills/brief-intake/templates/brief-runbook.md +44 -0
- package/skills/brief-intake/templates/brief-tech-comparison.md +38 -0
- package/skills/bug-triage/SKILL.md +67 -0
- package/skills/bug-triage/manifest.json +11 -0
- package/skills/bug-triage/templates/bug-ticket.md +86 -0
- package/skills/business-case/SKILL.md +64 -0
- package/skills/business-case/manifest.json +11 -0
- package/skills/business-case/templates/business-case.md +60 -0
- package/skills/capacity-cost-model/SKILL.md +64 -0
- package/skills/capacity-cost-model/manifest.json +11 -0
- package/skills/capacity-cost-model/reference.md +54 -0
- package/skills/capacity-cost-model/templates/cost-model.md +68 -0
- package/skills/council-review/SKILL.md +106 -0
- package/skills/council-review/examples/redis-cache-decision.md +75 -0
- package/skills/council-review/manifest.json +11 -0
- package/skills/council-review/reference.md +76 -0
- package/skills/council-review/seats/architect.md +29 -0
- package/skills/council-review/seats/bench.md +39 -0
- package/skills/council-review/seats/operator.md +27 -0
- package/skills/council-review/seats/pragmatist.md +29 -0
- package/skills/council-review/seats/security-engineer.md +29 -0
- package/skills/council-review/seats/user-advocate.md +27 -0
- package/skills/council-review/templates/council-review.md +59 -0
- package/skills/data-contract/SKILL.md +67 -0
- package/skills/data-contract/manifest.json +11 -0
- package/skills/data-contract/reference.md +46 -0
- package/skills/data-contract/templates/data-contract.md +69 -0
- package/skills/db-schema-design/SKILL.md +68 -0
- package/skills/db-schema-design/manifest.json +11 -0
- package/skills/db-schema-design/reference.md +56 -0
- package/skills/db-schema-design/templates/schema-design.md +75 -0
- package/skills/decision-log/SKILL.md +67 -0
- package/skills/decision-log/manifest.json +11 -0
- package/skills/deck-builder/SKILL.md +114 -0
- package/skills/deck-builder/examples/exec-board-update.md +108 -0
- package/skills/deck-builder/manifest.json +17 -0
- package/skills/deck-builder/reference.md +92 -0
- package/skills/deck-builder/requirements.txt +1 -0
- package/skills/deck-builder/scripts/build_deck.py +309 -0
- package/skills/deck-builder/templates/deck-spec.json +97 -0
- package/skills/deck-builder/templates/palettes.json +59 -0
- package/skills/design-doc/SKILL.md +49 -0
- package/skills/design-doc/manifest.json +11 -0
- package/skills/design-doc/templates/design-doc.md +98 -0
- package/skills/devils-advocate/SKILL.md +142 -0
- package/skills/devils-advocate/lenses/adversarial-review.md +88 -0
- package/skills/devils-advocate/lenses/edge-cases.md +95 -0
- package/skills/devils-advocate/lenses/future-proofing.md +75 -0
- package/skills/devils-advocate/lenses/test-coverage.md +90 -0
- package/skills/devils-advocate/manifest.json +11 -0
- package/skills/devils-advocate/templates/challenge.md +118 -0
- package/skills/doc-critique/SKILL.md +119 -0
- package/skills/doc-critique/manifest.json +11 -0
- package/skills/doc-critique/rubrics/adr.md +36 -0
- package/skills/doc-critique/rubrics/decision-log.md +39 -0
- package/skills/doc-critique/rubrics/deck.md +45 -0
- package/skills/doc-critique/rubrics/design-doc.md +42 -0
- package/skills/doc-critique/rubrics/exec-summary.md +41 -0
- package/skills/doc-critique/rubrics/generic.md +40 -0
- package/skills/doc-critique/rubrics/runbook.md +42 -0
- package/skills/doc-critique/rubrics/tech-comparison.md +40 -0
- package/skills/doc-critique/templates/critique.md +49 -0
- package/skills/exec-summary/SKILL.md +45 -0
- package/skills/exec-summary/manifest.json +11 -0
- package/skills/exec-summary/templates/exec-summary.md +48 -0
- package/skills/feature-implement-loop/SKILL.md +83 -0
- package/skills/feature-implement-loop/manifest.json +11 -0
- package/skills/figma-diagrams/SKILL.md +85 -0
- package/skills/figma-diagrams/manifest.json +35 -0
- package/skills/figma-diagrams/reference.md +91 -0
- package/skills/figma-diagrams/requirements.txt +1 -0
- package/skills/figma-diagrams/scripts/_figma_client.py +71 -0
- package/skills/figma-diagrams/scripts/frame_to_mermaid.py +102 -0
- package/skills/figma-diagrams/scripts/inspect_file.py +46 -0
- package/skills/figma-diagrams/scripts/list_comments.py +35 -0
- package/skills/figma-diagrams/scripts/post_comment.py +52 -0
- package/skills/figma-diagrams/templates/figjam-import.json +20 -0
- package/skills/guide-builder/SKILL.md +76 -0
- package/skills/guide-builder/examples/local-setup-guide.md +127 -0
- package/skills/guide-builder/manifest.json +11 -0
- package/skills/guide-builder/templates/api-reference.md +120 -0
- package/skills/guide-builder/templates/design-walkthrough.md +72 -0
- package/skills/guide-builder/templates/setup-guide.md +109 -0
- package/skills/incident-comms/SKILL.md +68 -0
- package/skills/incident-comms/manifest.json +11 -0
- package/skills/incident-comms/reference.md +43 -0
- package/skills/incident-comms/templates/messages.md +82 -0
- package/skills/llm-eval-harness/SKILL.md +73 -0
- package/skills/llm-eval-harness/manifest.json +11 -0
- package/skills/llm-eval-harness/reference.md +58 -0
- package/skills/llm-eval-harness/templates/eval-plan.md +63 -0
- package/skills/llm-eval-harness/templates/golden-case.jsonl +5 -0
- package/skills/migration-plan/SKILL.md +68 -0
- package/skills/migration-plan/manifest.json +11 -0
- package/skills/migration-plan/templates/migration-plan.md +68 -0
- package/skills/nfr-spec/SKILL.md +63 -0
- package/skills/nfr-spec/manifest.json +11 -0
- package/skills/nfr-spec/reference.md +74 -0
- package/skills/nfr-spec/templates/nfr-spec.md +56 -0
- package/skills/observability-plan/SKILL.md +70 -0
- package/skills/observability-plan/manifest.json +11 -0
- package/skills/observability-plan/reference.md +67 -0
- package/skills/observability-plan/templates/observability-plan.md +62 -0
- package/skills/okr-cascade/SKILL.md +59 -0
- package/skills/okr-cascade/evals/eval_queries.json +12 -0
- package/skills/okr-cascade/evals/evals.json +28 -0
- package/skills/okr-cascade/manifest.json +11 -0
- package/skills/okr-cascade/templates/okr-cascade.md +46 -0
- package/skills/postmortem-generator/SKILL.md +65 -0
- package/skills/postmortem-generator/manifest.json +11 -0
- package/skills/postmortem-generator/templates/postmortem.md +67 -0
- package/skills/prd-draft/SKILL.md +62 -0
- package/skills/prd-draft/manifest.json +11 -0
- package/skills/prd-draft/templates/prd.md +66 -0
- package/skills/prfaq/SKILL.md +75 -0
- package/skills/prfaq/evals/eval_queries.json +13 -0
- package/skills/prfaq/evals/evals.json +32 -0
- package/skills/prfaq/manifest.json +11 -0
- package/skills/prfaq/templates/prfaq.md +60 -0
- package/skills/release-notes/SKILL.md +61 -0
- package/skills/release-notes/manifest.json +11 -0
- package/skills/release-notes/templates/release-notes.md +83 -0
- package/skills/requirements-interview/SKILL.md +61 -0
- package/skills/requirements-interview/manifest.json +11 -0
- package/skills/requirements-interview/templates/interview-kit.md +63 -0
- package/skills/reverse-architecture/SKILL.md +103 -0
- package/skills/reverse-architecture/manifest.json +11 -0
- package/skills/reverse-architecture/reference.md +150 -0
- package/skills/reverse-architecture/templates/description.md +52 -0
- package/skills/reverse-architecture/templates/extraction.md +55 -0
- package/skills/runbook-generator/SKILL.md +42 -0
- package/skills/runbook-generator/manifest.json +11 -0
- package/skills/runbook-generator/templates/runbook.md +109 -0
- package/skills/slide-outliner/SKILL.md +58 -0
- package/skills/slide-outliner/manifest.json +11 -0
- package/skills/slide-outliner/templates/deck-outline.md +131 -0
- package/skills/sonar-onboard/SKILL.md +170 -0
- package/skills/sonar-onboard/manifest.json +11 -0
- package/skills/sonar-onboard/reference.md +151 -0
- package/skills/sonar-onboard/templates/github-actions-sonar-server.yml.tmpl +50 -0
- package/skills/sonar-onboard/templates/github-actions-sonarcloud.yml.tmpl +53 -0
- package/skills/sonar-onboard/templates/readme-snippet.md +38 -0
- package/skills/sonar-onboard/templates/sonar-project.properties.tmpl +31 -0
- package/skills/sonar-review/SKILL.md +220 -0
- package/skills/sonar-review/lenses/bugs.md +48 -0
- package/skills/sonar-review/lenses/code-smells.md +64 -0
- package/skills/sonar-review/lenses/coverage-and-duplication.md +74 -0
- package/skills/sonar-review/lenses/security-hotspots.md +56 -0
- package/skills/sonar-review/lenses/vulnerabilities.md +52 -0
- package/skills/sonar-review/manifest.json +37 -0
- package/skills/sonar-review/reference.md +167 -0
- package/skills/sonar-review/templates/report.md +84 -0
- package/skills/strategy-analysis/SKILL.md +63 -0
- package/skills/strategy-analysis/evals/eval_queries.json +13 -0
- package/skills/strategy-analysis/evals/evals.json +36 -0
- package/skills/strategy-analysis/manifest.json +11 -0
- package/skills/success-metrics/SKILL.md +63 -0
- package/skills/success-metrics/evals/eval_queries.json +11 -0
- package/skills/success-metrics/evals/evals.json +27 -0
- package/skills/success-metrics/manifest.json +11 -0
- package/skills/success-metrics/templates/metrics-plan.md +55 -0
- package/skills/tech-comparison-matrix/SKILL.md +56 -0
- package/skills/tech-comparison-matrix/manifest.json +11 -0
- package/skills/tech-comparison-matrix/templates/matrix.md +45 -0
- package/skills/test-plan-generator/SKILL.md +59 -0
- package/skills/test-plan-generator/manifest.json +11 -0
- package/skills/test-plan-generator/templates/test-plan.md +83 -0
- package/skills/threat-model/SKILL.md +65 -0
- package/skills/threat-model/examples/document-upload.md +56 -0
- package/skills/threat-model/manifest.json +11 -0
- package/skills/threat-model/reference.md +69 -0
- package/skills/user-journey-map/SKILL.md +64 -0
- package/skills/user-journey-map/evals/eval_queries.json +12 -0
- package/skills/user-journey-map/evals/evals.json +30 -0
- package/skills/user-journey-map/examples/b2b-saas-onboarding.md +69 -0
- package/skills/user-journey-map/manifest.json +11 -0
- package/skills/user-journey-map/templates/journey-map.md +47 -0
- package/skills/user-story-splitter/SKILL.md +88 -0
- package/skills/user-story-splitter/examples/notifications-epic.md +123 -0
- package/skills/user-story-splitter/manifest.json +11 -0
- 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.
|