yield-audit 0.3.2__py3-none-any.whl
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.
- yield_audit/__init__.py +3 -0
- yield_audit/__main__.py +8 -0
- yield_audit/attribute.py +117 -0
- yield_audit/audit.py +453 -0
- yield_audit/cli.py +230 -0
- yield_audit/cohorts.py +70 -0
- yield_audit/costs.py +52 -0
- yield_audit/events.py +107 -0
- yield_audit/gitdata.py +335 -0
- yield_audit/lenses/AGENTS.md +35 -0
- yield_audit/lenses/__init__.py +1 -0
- yield_audit/lenses/accepted.py +79 -0
- yield_audit/lenses/cache_locality.py +112 -0
- yield_audit/lenses/retry.py +82 -0
- yield_audit/lenses/rework.py +152 -0
- yield_audit/lenses/survival.py +239 -0
- yield_audit/lenses/verify_gap.py +101 -0
- yield_audit/lenses/waste.py +102 -0
- yield_audit/pricing.py +124 -0
- yield_audit/redact.py +117 -0
- yield_audit/report.py +239 -0
- yield_audit/transcripts/__init__.py +130 -0
- yield_audit/transcripts/base.py +208 -0
- yield_audit/transcripts/claude.py +160 -0
- yield_audit/transcripts/codex.py +195 -0
- yield_audit-0.3.2.dist-info/METADATA +197 -0
- yield_audit-0.3.2.dist-info/RECORD +30 -0
- yield_audit-0.3.2.dist-info/WHEEL +4 -0
- yield_audit-0.3.2.dist-info/entry_points.txt +2 -0
- yield_audit-0.3.2.dist-info/licenses/LICENSE +202 -0
yield_audit/__init__.py
ADDED
yield_audit/__main__.py
ADDED
yield_audit/attribute.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Session-to-commit attribution with explicit confidence grades.
|
|
2
|
+
|
|
3
|
+
Attribution is probabilistic by nature, so every pair carries a grade and the
|
|
4
|
+
report states it. Rules (v0.1, deterministic):
|
|
5
|
+
|
|
6
|
+
- Candidates: sessions overlapping the commit date (session must start no more
|
|
7
|
+
than ``grace`` after the commit, and end no earlier than ``window`` before
|
|
8
|
+
it) that share at least one edited file with the commit.
|
|
9
|
+
- ``high`` — the session itself ran ``git commit`` AND shares edited files.
|
|
10
|
+
- ``medium``— time proximity + shared edited files.
|
|
11
|
+
- A commit claimed by several same-grade sessions is split evenly across them
|
|
12
|
+
and flagged ambiguous; higher grades win over lower grades regardless of
|
|
13
|
+
overlap counts.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from datetime import timedelta
|
|
20
|
+
|
|
21
|
+
from .events import Session
|
|
22
|
+
from .gitdata import CommitInfo
|
|
23
|
+
|
|
24
|
+
GRADES = ("high", "medium", "none")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Attribution:
|
|
29
|
+
session_id: str
|
|
30
|
+
commit_sha: str
|
|
31
|
+
grade: str
|
|
32
|
+
share: float # 1.0, or 1/n when contested by equal-grade sessions
|
|
33
|
+
shared_files: list[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class AttributionResult:
|
|
38
|
+
pairs: list[Attribution]
|
|
39
|
+
ambiguous_commits: list[str]
|
|
40
|
+
claimed_shas: set[str]
|
|
41
|
+
unclaimed_commits: list[str]
|
|
42
|
+
|
|
43
|
+
def for_session(self, session_id: str) -> list[Attribution]:
|
|
44
|
+
return [p for p in self.pairs if p.session_id == session_id]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def attribute(
|
|
48
|
+
sessions: list[Session],
|
|
49
|
+
commits: list[CommitInfo],
|
|
50
|
+
*,
|
|
51
|
+
proximity: timedelta = timedelta(hours=24),
|
|
52
|
+
start_grace: timedelta = timedelta(minutes=5),
|
|
53
|
+
) -> AttributionResult:
|
|
54
|
+
# Commit files as sets, sessions' edited files as sets.
|
|
55
|
+
commit_files = {c.sha: set(c.files) for c in commits}
|
|
56
|
+
session_files = {s.session_id: set(s.edited_files) for s in sessions}
|
|
57
|
+
|
|
58
|
+
claims: dict[str, list[tuple[str, str, list[str]]]] = {} # sha -> [(session_id, grade, shared_files)]
|
|
59
|
+
for commit in commits:
|
|
60
|
+
for session in sessions:
|
|
61
|
+
edited = session_files[session.session_id]
|
|
62
|
+
if not edited:
|
|
63
|
+
continue
|
|
64
|
+
overlap = edited & commit_files[commit.sha]
|
|
65
|
+
if not overlap:
|
|
66
|
+
continue
|
|
67
|
+
if session.start > commit.date + start_grace:
|
|
68
|
+
continue
|
|
69
|
+
if session.end < commit.date - proximity:
|
|
70
|
+
continue
|
|
71
|
+
grade = "high" if session.ran_git_commit else "medium"
|
|
72
|
+
claims.setdefault(commit.sha, []).append(
|
|
73
|
+
(session.session_id, grade, sorted(overlap))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
pairs: list[Attribution] = []
|
|
77
|
+
ambiguous: list[str] = []
|
|
78
|
+
for commit in commits:
|
|
79
|
+
claim_list = claims.get(commit.sha, [])
|
|
80
|
+
if not claim_list:
|
|
81
|
+
continue
|
|
82
|
+
best_grade = min((g for _, g, _ in claim_list), key=GRADES.index)
|
|
83
|
+
# Every same-grade claimant keeps a stake, split evenly: dropping
|
|
84
|
+
# lower-overlap claimants would silently erase their output from
|
|
85
|
+
# share-weighted aggregates downstream. Overlap counts do not weigh
|
|
86
|
+
# the split — under v0.1 heuristics any weighting would be fake
|
|
87
|
+
# precision.
|
|
88
|
+
winners = [(sid, grade, shared) for sid, grade, shared in claim_list if grade == best_grade]
|
|
89
|
+
winners.sort(key=lambda item: item[0]) # deterministic order
|
|
90
|
+
share = 1.0
|
|
91
|
+
if len(winners) > 1:
|
|
92
|
+
ambiguous.append(commit.sha)
|
|
93
|
+
share = 1.0 / len(winners)
|
|
94
|
+
for sid, grade, shared in winners:
|
|
95
|
+
pairs.append(
|
|
96
|
+
Attribution(
|
|
97
|
+
session_id=sid,
|
|
98
|
+
commit_sha=commit.sha,
|
|
99
|
+
grade=grade,
|
|
100
|
+
share=share,
|
|
101
|
+
shared_files=shared,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
pairs.sort(key=lambda p: (p.commit_sha, p.session_id))
|
|
106
|
+
claimed = {p.commit_sha for p in pairs}
|
|
107
|
+
unclaimed = [c.sha for c in commits if c.sha not in claimed]
|
|
108
|
+
return AttributionResult(
|
|
109
|
+
pairs=pairs,
|
|
110
|
+
ambiguous_commits=sorted(ambiguous),
|
|
111
|
+
claimed_shas=claimed,
|
|
112
|
+
unclaimed_commits=unclaimed,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def commits_by_sha(commits: list[CommitInfo]) -> dict[str, CommitInfo]:
|
|
117
|
+
return {c.sha: c for c in commits}
|
yield_audit/audit.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Audit pipeline: normalize inputs, run lenses, assemble the report dict.
|
|
2
|
+
|
|
3
|
+
Every metric block carries a ``measurement`` label so no reader can mistake
|
|
4
|
+
an estimate for an observation:
|
|
5
|
+
|
|
6
|
+
- ``observed`` — read straight from local records (token counts, commands).
|
|
7
|
+
- ``estimate`` — observed values × list-price assumptions (USD figures).
|
|
8
|
+
- ``proxy`` — a stated substitute stands in for an unobservable quantity
|
|
9
|
+
(e.g. line-share standing in for per-commit token share).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime, timedelta, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import cohorts, gitdata, redact, transcripts
|
|
18
|
+
from .attribute import attribute as attribute_fn
|
|
19
|
+
from .costs import session_cost
|
|
20
|
+
from .lenses.accepted import analyze_accepted
|
|
21
|
+
from .lenses.cache_locality import analyze_cache_locality
|
|
22
|
+
from .lenses.retry import analyze_retry
|
|
23
|
+
from .lenses.rework import analyze_rework
|
|
24
|
+
from .lenses.survival import analyze_survival
|
|
25
|
+
from .lenses.verify_gap import analyze_verify_gap
|
|
26
|
+
from .lenses.waste import analyze_waste
|
|
27
|
+
from .pricing import load_pricing
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION = "yieldaudit.report.v1"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuditError(RuntimeError):
|
|
33
|
+
"""User-facing configuration/environment problem."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_audit(
|
|
37
|
+
*,
|
|
38
|
+
repo: str,
|
|
39
|
+
transcripts_root: Path | None,
|
|
40
|
+
days: int,
|
|
41
|
+
horizons: tuple[int, ...],
|
|
42
|
+
headline_horizon: int,
|
|
43
|
+
now: datetime,
|
|
44
|
+
pricing_override: str | None,
|
|
45
|
+
proximity_hours: int,
|
|
46
|
+
show_paths: bool,
|
|
47
|
+
details: bool,
|
|
48
|
+
agents=None,
|
|
49
|
+
rework_days: int = 14,
|
|
50
|
+
log=None,
|
|
51
|
+
) -> dict:
|
|
52
|
+
if not gitdata.is_git_repo(repo):
|
|
53
|
+
raise AuditError(f"not a git repository: {repo}")
|
|
54
|
+
if headline_horizon not in horizons:
|
|
55
|
+
horizons = (headline_horizon, *horizons)
|
|
56
|
+
|
|
57
|
+
repo_real = transcripts.normalize_path(repo)
|
|
58
|
+
log_messages: list[str] = []
|
|
59
|
+
if transcripts_root is not None and not Path(transcripts_root).is_dir():
|
|
60
|
+
raise AuditError(f"transcripts dir not found: {transcripts_root}")
|
|
61
|
+
roots = transcripts.resolve_roots(agents or transcripts.DEFAULT_AGENTS, transcripts_root)
|
|
62
|
+
if not roots:
|
|
63
|
+
tried = ", ".join(
|
|
64
|
+
f"{name} {transcripts.ADAPTERS[name].default_root()}"
|
|
65
|
+
for name in sorted(agents or transcripts.DEFAULT_AGENTS)
|
|
66
|
+
)
|
|
67
|
+
raise AuditError(
|
|
68
|
+
f"no agent transcripts found (tried: {tried}) — "
|
|
69
|
+
"pass --transcripts-dir or run `yield-audit doctor`"
|
|
70
|
+
)
|
|
71
|
+
for name in sorted(set(transcripts.DEFAULT_AGENTS) - set(roots)):
|
|
72
|
+
log_messages.append(f"agent {name}: transcripts root not found, skipped")
|
|
73
|
+
if log is not None:
|
|
74
|
+
sessions = transcripts.load_sessions(
|
|
75
|
+
repo_real, transcripts_root, now=now, days=days, agents=agents,
|
|
76
|
+
logger=lambda m: (log_messages.append(m), log(m))[1],
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
sessions = transcripts.load_sessions(
|
|
80
|
+
repo_real, transcripts_root, now=now, days=days, agents=agents,
|
|
81
|
+
logger=log_messages.append,
|
|
82
|
+
)
|
|
83
|
+
if not sessions:
|
|
84
|
+
log_messages.append(
|
|
85
|
+
"0 agent sessions matched this repo in the window — "
|
|
86
|
+
"run `yield-audit doctor --repo <repo>` to check transcript discovery"
|
|
87
|
+
)
|
|
88
|
+
transcripts.group_edit_files_by_repo(sessions, repo_real)
|
|
89
|
+
|
|
90
|
+
since = now - timedelta(days=days) if days and days > 0 else None
|
|
91
|
+
git_warnings: list[str] = []
|
|
92
|
+
commits = gitdata.commits_with_numstat(repo_real, since=since, until=None, warnings=git_warnings)
|
|
93
|
+
commits_by_sha = {c.sha: c for c in commits}
|
|
94
|
+
|
|
95
|
+
attributions = attribute_fn(
|
|
96
|
+
sessions,
|
|
97
|
+
commits,
|
|
98
|
+
proximity=timedelta(hours=proximity_hours),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
messages = gitdata.commit_messages(repo_real, since=since, until=None)
|
|
102
|
+
cohort_labels = cohorts.label_commits(messages, attributions.claimed_shas)
|
|
103
|
+
|
|
104
|
+
table, fallback, pricing_notes = load_pricing(pricing_override)
|
|
105
|
+
|
|
106
|
+
session_costs = {}
|
|
107
|
+
cost_objects = {}
|
|
108
|
+
for session in sessions:
|
|
109
|
+
cost = session_cost(session, table, fallback)
|
|
110
|
+
cost_objects[session.session_id] = cost
|
|
111
|
+
session_costs[session.session_id] = {
|
|
112
|
+
"cost_usd": cost.cost_usd,
|
|
113
|
+
"total_tokens": cost.total_input_tokens + cost.output_tokens,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# One shared cache across lenses: blame/snapshot results plus the
|
|
117
|
+
# single touch-map pass that lets both skip blaming files no commit
|
|
118
|
+
# touched inside the measurement window.
|
|
119
|
+
blame_cache: dict = {}
|
|
120
|
+
survival = analyze_survival(
|
|
121
|
+
repo_real,
|
|
122
|
+
attributions,
|
|
123
|
+
commits_by_sha,
|
|
124
|
+
now=now,
|
|
125
|
+
horizons=horizons,
|
|
126
|
+
headline_horizon=headline_horizon,
|
|
127
|
+
blame_cache=blame_cache,
|
|
128
|
+
touch_since=since,
|
|
129
|
+
)
|
|
130
|
+
waste = analyze_waste(survival, {sid: c["cost_usd"] for sid, c in session_costs.items()}, headline_horizon)
|
|
131
|
+
|
|
132
|
+
commit_dates: dict[str, datetime] = {}
|
|
133
|
+
committed_ids: set[str] = set()
|
|
134
|
+
for pair in attributions.pairs:
|
|
135
|
+
committed_ids.add(pair.session_id)
|
|
136
|
+
commit = commits_by_sha.get(pair.commit_sha)
|
|
137
|
+
if commit is not None:
|
|
138
|
+
prev = commit_dates.get(pair.session_id)
|
|
139
|
+
commit_dates[pair.session_id] = max(prev, commit.date) if prev else commit.date
|
|
140
|
+
|
|
141
|
+
survival_rates = {
|
|
142
|
+
sid: info["rate"] if info["added"] > 0 else None
|
|
143
|
+
for sid, info in survival.sessions.items()
|
|
144
|
+
}
|
|
145
|
+
verify = analyze_verify_gap(sessions, commit_dates, survival_rates)
|
|
146
|
+
accepted = analyze_accepted(session_costs, survival.sessions, committed_ids)
|
|
147
|
+
|
|
148
|
+
retry_by_session = {s.session_id: analyze_retry(s) for s in sessions}
|
|
149
|
+
cache_by_session = {
|
|
150
|
+
s.session_id: analyze_cache_locality(s, table, fallback) for s in sessions
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
rework = analyze_rework(
|
|
154
|
+
repo_real,
|
|
155
|
+
commits,
|
|
156
|
+
cohort_labels,
|
|
157
|
+
now=now,
|
|
158
|
+
horizon_days=rework_days,
|
|
159
|
+
blame_cache=blame_cache,
|
|
160
|
+
touch_since=since,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
unknown_models: set[str] = set()
|
|
164
|
+
for cost in cost_objects.values():
|
|
165
|
+
unknown_models |= cost.unknown_models
|
|
166
|
+
|
|
167
|
+
report = {
|
|
168
|
+
"schema_version": SCHEMA_VERSION,
|
|
169
|
+
"generated_at": now.astimezone(timezone.utc).isoformat(),
|
|
170
|
+
"parameters": {
|
|
171
|
+
"repo": repo_real if show_paths else redact.abbreviate_home(repo_real),
|
|
172
|
+
"window_days": days,
|
|
173
|
+
"horizons_days": list(horizons),
|
|
174
|
+
"headline_horizon_days": headline_horizon,
|
|
175
|
+
"agents_scanned": sorted(roots),
|
|
176
|
+
"transcripts_root": redact.abbreviate_home(str(roots[sorted(roots)[0]])) if roots else "",
|
|
177
|
+
"transcripts_roots": {
|
|
178
|
+
name: redact.abbreviate_home(str(root)) for name, root in sorted(roots.items())
|
|
179
|
+
},
|
|
180
|
+
"attribution_proximity_hours": proximity_hours,
|
|
181
|
+
"rework_horizon_days": rework_days,
|
|
182
|
+
"pricing_source": "builtin_2026-09" if not pricing_override else str(pricing_override),
|
|
183
|
+
},
|
|
184
|
+
"input": {
|
|
185
|
+
"sessions": len(sessions),
|
|
186
|
+
"api_calls": sum(len(s.api_calls) for s in sessions),
|
|
187
|
+
"commits_in_window": len(commits),
|
|
188
|
+
"attributed_commits": len(attributions.claimed_shas),
|
|
189
|
+
"unclaimed_commits": len(attributions.unclaimed_commits),
|
|
190
|
+
"ambiguous_commits": attributions.ambiguous_commits,
|
|
191
|
+
"unknown_models": sorted(redact.sanitize_text(m) for m in unknown_models),
|
|
192
|
+
},
|
|
193
|
+
"attribution": _attribution_block(attributions),
|
|
194
|
+
"m1_survival": _survival_block(survival, show_paths, details),
|
|
195
|
+
"m2_waste": _waste_block(waste),
|
|
196
|
+
"m3_retry": _retry_block(retry_by_session, show_paths),
|
|
197
|
+
"m4_accepted": _accepted_block(accepted),
|
|
198
|
+
"m5_cache": _cache_block(cache_by_session),
|
|
199
|
+
"m8_verify": _verify_block(verify),
|
|
200
|
+
"m11_rework": _rework_block(rework, details),
|
|
201
|
+
"notes": _global_notes(pricing_notes) + git_warnings + log_messages[:20],
|
|
202
|
+
}
|
|
203
|
+
# Defense in depth: nothing in the report bypasses the output boundary,
|
|
204
|
+
# even if a future field forgets to sanitize.
|
|
205
|
+
return redact.deep_sanitize(report)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _attribution_block(attributions) -> dict:
|
|
209
|
+
grades: dict[str, int] = {}
|
|
210
|
+
for pair in attributions.pairs:
|
|
211
|
+
grades[pair.grade] = grades.get(pair.grade, 0) + 1
|
|
212
|
+
return {
|
|
213
|
+
"measurement": "heuristic_matching_with_confidence_grades",
|
|
214
|
+
"pairs": len(attributions.pairs),
|
|
215
|
+
"grades": grades,
|
|
216
|
+
"ambiguous_commits": attributions.ambiguous_commits,
|
|
217
|
+
"note": "high = session ran the commit itself; medium = shared edited files within the time window; contested commits are split and flagged",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _survival_block(survival, show_paths: bool, details: bool) -> dict:
|
|
222
|
+
block = {
|
|
223
|
+
"measurement": "measured_from_git_history",
|
|
224
|
+
"horizon_days": survival.horizon,
|
|
225
|
+
"overall_rate": _round(survival.overall),
|
|
226
|
+
"added_lines": _num(survival.overall_added),
|
|
227
|
+
"survived_lines": _num(survival.overall_survived),
|
|
228
|
+
"pending_units": survival.pending_count,
|
|
229
|
+
"by_kind": {
|
|
230
|
+
kind: {
|
|
231
|
+
"added": _num(info["added"]),
|
|
232
|
+
"survived": _num(info["survived"]),
|
|
233
|
+
"rate": _round(info["rate"]),
|
|
234
|
+
}
|
|
235
|
+
for kind, info in survival.by_kind.items()
|
|
236
|
+
},
|
|
237
|
+
"per_session": {
|
|
238
|
+
_sid(sid): {
|
|
239
|
+
"added": _num(info["added"]),
|
|
240
|
+
"survived": _num(info["survived"]),
|
|
241
|
+
"rate": _round(info["rate"]),
|
|
242
|
+
"pending": info["pending"],
|
|
243
|
+
}
|
|
244
|
+
for sid, info in sorted(survival.sessions.items())
|
|
245
|
+
},
|
|
246
|
+
"notes": survival.notes,
|
|
247
|
+
}
|
|
248
|
+
if details:
|
|
249
|
+
block["units"] = [
|
|
250
|
+
{
|
|
251
|
+
"session": _sid(u.session_id),
|
|
252
|
+
"path": redact.redact_path(u.path, show_paths=show_paths),
|
|
253
|
+
"kind": u.kind,
|
|
254
|
+
"added": u.added,
|
|
255
|
+
"survived": u.survived.get(survival.horizon),
|
|
256
|
+
"deleted": u.deleted.get(survival.horizon),
|
|
257
|
+
"pending": survival.horizon in u.pending_horizons,
|
|
258
|
+
}
|
|
259
|
+
for u in survival.units
|
|
260
|
+
]
|
|
261
|
+
return block
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _waste_block(waste) -> dict:
|
|
265
|
+
total_lower = sum(b.lower_usd for b in waste.values())
|
|
266
|
+
total_upper = sum(b.upper_usd for b in waste.values())
|
|
267
|
+
block = {
|
|
268
|
+
"measurement": "estimate_with_bounds",
|
|
269
|
+
"total_lower_usd": round(total_lower, 6),
|
|
270
|
+
"total_upper_usd": round(total_upper, 6),
|
|
271
|
+
"method": "session cost x attribution-share-weighted line-share proxy x waste class (removed=lower+upper, rewritten>=50% lost=upper only)",
|
|
272
|
+
"per_session": {
|
|
273
|
+
_sid(sid): {
|
|
274
|
+
"lower_usd": round(b.lower_usd, 6),
|
|
275
|
+
"upper_usd": round(b.upper_usd, 6),
|
|
276
|
+
"removed_lines": _num(b.removed_lines),
|
|
277
|
+
"rewritten_lines": _num(b.rewritten_lines),
|
|
278
|
+
"edited_lines": _num(b.edited_lines),
|
|
279
|
+
}
|
|
280
|
+
for sid, b in sorted(waste.items())
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
return block
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _retry_block(retry_by_session, show_paths: bool) -> dict:
|
|
287
|
+
total_tax_tokens = sum(r.tax_tokens for r in retry_by_session.values())
|
|
288
|
+
total_tokens = sum(r.total_tokens for r in retry_by_session.values())
|
|
289
|
+
chains = [
|
|
290
|
+
{
|
|
291
|
+
"session": _sid(sid),
|
|
292
|
+
"command": redact.sanitize_command(c.command, show_paths=show_paths, limit=80),
|
|
293
|
+
"attempts": c.attempts,
|
|
294
|
+
"errors": c.errors,
|
|
295
|
+
}
|
|
296
|
+
for sid, r in sorted(retry_by_session.items())
|
|
297
|
+
for c in r.chains
|
|
298
|
+
]
|
|
299
|
+
return {
|
|
300
|
+
"measurement": "observed_from_transcripts",
|
|
301
|
+
"total_tax_tokens": total_tax_tokens,
|
|
302
|
+
"total_tokens": total_tokens,
|
|
303
|
+
"tax_share": _round(total_tax_tokens / total_tokens) if total_tokens else None,
|
|
304
|
+
"failure_chains": chains[:200],
|
|
305
|
+
"chains_truncated": max(0, len(chains) - 200),
|
|
306
|
+
"per_session": {
|
|
307
|
+
_sid(sid): {
|
|
308
|
+
"tax_tokens": r.tax_tokens,
|
|
309
|
+
"tax_share": _round(r.tax_token_share),
|
|
310
|
+
"chains": len(r.chains),
|
|
311
|
+
}
|
|
312
|
+
for sid, r in sorted(retry_by_session.items())
|
|
313
|
+
if r.chains
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _accepted_block(accepted) -> dict:
|
|
319
|
+
return {
|
|
320
|
+
"measurement": "estimate_observed_tokens_x_list_price",
|
|
321
|
+
"cost_per_accepted_usd": _round(accepted.cost_per_accepted_usd, 6),
|
|
322
|
+
"tokens_per_accepted": accepted.tokens_per_accepted,
|
|
323
|
+
"accept_threshold_survival": 0.5,
|
|
324
|
+
"totals": {
|
|
325
|
+
status: {
|
|
326
|
+
"sessions": info["sessions"],
|
|
327
|
+
"cost_usd": _round(info["cost_usd"], 6),
|
|
328
|
+
"total_tokens": info["total_tokens"],
|
|
329
|
+
}
|
|
330
|
+
for status, info in accepted.totals.items()
|
|
331
|
+
},
|
|
332
|
+
"per_session": {
|
|
333
|
+
_sid(sid): {
|
|
334
|
+
"status": info["status"],
|
|
335
|
+
"cost_usd": _round(info["cost_usd"], 6),
|
|
336
|
+
"survival_rate": _round(info["survival_rate"]),
|
|
337
|
+
}
|
|
338
|
+
for sid, info in sorted(accepted.sessions.items())
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _cache_block(cache_by_session) -> dict:
|
|
344
|
+
total_wasted = sum(r.wasted_usd for r in cache_by_session.values())
|
|
345
|
+
cold_total = sum(r.cold_calls for r in cache_by_session.values())
|
|
346
|
+
by_class: dict[str, int] = {}
|
|
347
|
+
for r in cache_by_session.values():
|
|
348
|
+
for cls, count in r.by_class.items():
|
|
349
|
+
by_class[cls] = by_class.get(cls, 0) + count
|
|
350
|
+
rates = [r.hit_rate for r in cache_by_session.values() if r.hit_rate is not None]
|
|
351
|
+
events = []
|
|
352
|
+
for sid, r in sorted(cache_by_session.items()):
|
|
353
|
+
for e in r.events:
|
|
354
|
+
if e.class_name == "compaction":
|
|
355
|
+
continue
|
|
356
|
+
events.append(
|
|
357
|
+
{
|
|
358
|
+
"session": _sid(sid),
|
|
359
|
+
"ts": e.ts.isoformat(),
|
|
360
|
+
"class": e.class_name,
|
|
361
|
+
"gap_seconds": round(e.gap_seconds) if e.gap_seconds is not None else None,
|
|
362
|
+
"input_tokens": e.input_tokens,
|
|
363
|
+
"wasted_usd": _round(e.wasted_usd, 6),
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
"measurement": "estimate_observed_tokens_x_list_price",
|
|
368
|
+
"cold_calls": cold_total,
|
|
369
|
+
"cold_by_class": by_class,
|
|
370
|
+
"wasted_usd": _round(total_wasted, 6),
|
|
371
|
+
"mean_session_hit_rate": _round(sum(rates) / len(rates)) if rates else None,
|
|
372
|
+
"events": events[:200],
|
|
373
|
+
"events_truncated": max(0, len(events) - 200),
|
|
374
|
+
"notes": [
|
|
375
|
+
"wasted_usd = what non-compaction cold input would have cost at cache-read price; compaction rebuilds are excluded by design"
|
|
376
|
+
],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _rework_block(rework, details: bool) -> dict:
|
|
381
|
+
block = {
|
|
382
|
+
"measurement": "measured_from_git_history",
|
|
383
|
+
"rework_horizon_days": rework.horizon_days,
|
|
384
|
+
"cohort_evidence": dict(sorted(rework.evidence.items())),
|
|
385
|
+
"cohorts": {
|
|
386
|
+
label: {
|
|
387
|
+
"commits": info["commits"],
|
|
388
|
+
"measured_commits": info["measured_commits"],
|
|
389
|
+
"pending_commits": info["pending_commits"],
|
|
390
|
+
"added_lines": _num(info["added"]),
|
|
391
|
+
"reworked_lines": _num(info["reworked"]),
|
|
392
|
+
"rework_rate": _round(info["rate"]),
|
|
393
|
+
}
|
|
394
|
+
for label, info in sorted(rework.cohorts.items())
|
|
395
|
+
},
|
|
396
|
+
"notes": rework.notes,
|
|
397
|
+
}
|
|
398
|
+
if details:
|
|
399
|
+
block["commits"] = rework.commits[:200]
|
|
400
|
+
block["commits_truncated"] = max(0, len(rework.commits) - 200)
|
|
401
|
+
return block
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _verify_block(verify) -> dict:
|
|
405
|
+
return {
|
|
406
|
+
"measurement": "observed_from_transcripts",
|
|
407
|
+
"gap_rate": _round(verify.gap_rate),
|
|
408
|
+
"gap_rate_strict": _round(verify.gap_rate_strict),
|
|
409
|
+
"per_session": {
|
|
410
|
+
_sid(sid): {
|
|
411
|
+
"status": info.status,
|
|
412
|
+
"verify_commands": info.verify_count,
|
|
413
|
+
}
|
|
414
|
+
for sid, info in sorted(verify.sessions.items())
|
|
415
|
+
},
|
|
416
|
+
"correlation_with_survival": {
|
|
417
|
+
status: {
|
|
418
|
+
"sessions": info["sessions"],
|
|
419
|
+
"mean_survival": _round(info["mean_survival"]),
|
|
420
|
+
}
|
|
421
|
+
for status, info in verify.correlation.items()
|
|
422
|
+
},
|
|
423
|
+
"notes": verify.notes,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _global_notes(pricing_notes: list[str]) -> list[str]:
|
|
428
|
+
return [
|
|
429
|
+
"all data stays local: transcripts and git history are read, nothing is uploaded",
|
|
430
|
+
"USD figures multiply observed token counts by list prices; your actual rates may differ (override with --pricing-file)",
|
|
431
|
+
"attribution is heuristic; every dependent metric inherits its confidence grades",
|
|
432
|
+
] + pricing_notes
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _round(value, digits: int = 6):
|
|
436
|
+
return round(value, digits) if isinstance(value, float) else value
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _num(value):
|
|
440
|
+
"""Collapse integral floats (share-weighted sums) to int for display."""
|
|
441
|
+
if isinstance(value, float) and value.is_integer():
|
|
442
|
+
return int(value)
|
|
443
|
+
return value
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _sid(session_id: str) -> str:
|
|
447
|
+
# Session ids are transcript-controlled; they end up as dict keys and
|
|
448
|
+
# table cells, so they pass the same sanitization as everything else.
|
|
449
|
+
# Vendor namespaced ids ("claude:abcd…") keep their prefix so reports
|
|
450
|
+
# stay unambiguous in multi-agent audits.
|
|
451
|
+
safe = redact.sanitize_text(session_id)
|
|
452
|
+
vendor, sep, rest = safe.partition(":")
|
|
453
|
+
return f"{vendor}:{rest[:8]}" if sep else safe[:8]
|