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