cctally 1.100.0 → 1.102.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/CHANGELOG.md +73 -0
- package/README.md +8 -2
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +1140 -282
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +53 -61
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +329 -111
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +151 -71
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +8 -1
- package/bin/_cctally_tui.py +425 -234
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_blocks.py +12 -0
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +62 -4
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_readme_refresh.py +26 -5
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/cctally +103 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
"""`cctally explain` — CLI parsing, terminal rendering and the wire adapter.
|
|
2
|
+
|
|
3
|
+
`diagnosis_to_wire` is the ONE serializer. The CLI and `GET /api/diagnosis`
|
|
4
|
+
both call it, which is what turns "the two surfaces agree" from a promise
|
|
5
|
+
into a byte comparison over `canonical_projection`.
|
|
6
|
+
|
|
7
|
+
Anonymization lives here rather than in either surface, for the same reason:
|
|
8
|
+
a project alias assigned at render time would differ between the terminal and
|
|
9
|
+
the route, and the equivalence check would then be comparing two different
|
|
10
|
+
privacy decisions.
|
|
11
|
+
|
|
12
|
+
Spec: docs/superpowers/specs/2026-08-19-620-s2-on-demand-diagnosis.md §4
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import datetime as dt
|
|
17
|
+
import decimal
|
|
18
|
+
import json
|
|
19
|
+
import sqlite3
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from types import MappingProxyType
|
|
23
|
+
from typing import Any, Mapping, Sequence
|
|
24
|
+
|
|
25
|
+
import _lib_diagnosis as kernel
|
|
26
|
+
from _cctally_core import _command_as_of
|
|
27
|
+
from _lib_json_envelope import stamp_schema_version
|
|
28
|
+
|
|
29
|
+
EXPLAIN_SCHEMA_VERSION = 1
|
|
30
|
+
|
|
31
|
+
# Paths the canonical projection drops. `measuredAt` is the instant of the
|
|
32
|
+
# read and legitimately differs between two surfaces reading the same facts;
|
|
33
|
+
# the rest are presentation-only.
|
|
34
|
+
CANONICAL_EXCLUDED_PATHS: tuple[str, ...] = (
|
|
35
|
+
"measuredAt",
|
|
36
|
+
"notes",
|
|
37
|
+
"window.label",
|
|
38
|
+
"results[].contributors[].subjectLabel",
|
|
39
|
+
"results[].classes[].rows[].subjectLabel",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# The projection is driven by the published constant itself, so the two cannot
|
|
43
|
+
# disagree. Dropping every key NAMED `label` at every depth also removed
|
|
44
|
+
# `constants.registry[].label`, which is a fact of the contract and not a
|
|
45
|
+
# presentation detail, and Task 13 proves byte-equality through this
|
|
46
|
+
# projection.
|
|
47
|
+
_EXCLUDED_PATHS = frozenset(CANONICAL_EXCLUDED_PATHS)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _cctally():
|
|
51
|
+
return sys.modules["cctally"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sources():
|
|
55
|
+
return _cctally()._load_sibling("_cctally_diagnosis_sources")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- anonymization ------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _alias_map(rows: Sequence[Any], prefix: str) -> dict[str, str]:
|
|
61
|
+
"""Deterministic response-local aliases, by descending USD then key.
|
|
62
|
+
|
|
63
|
+
Response-local means the alias means nothing outside this one report,
|
|
64
|
+
which is the property that makes it safe to paste. Ordering by cost keeps
|
|
65
|
+
`project-1` the largest contributor in every rendering of the same facts.
|
|
66
|
+
"""
|
|
67
|
+
ranked = sorted(
|
|
68
|
+
{(r.subject_key, _usd(r)) for r in rows},
|
|
69
|
+
key=lambda pair: (-pair[1], pair[0]),
|
|
70
|
+
)
|
|
71
|
+
return {key: f"{prefix}-{index}"
|
|
72
|
+
for index, (key, _usd_value) in enumerate(ranked, start=1)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _usd(row: Any) -> float:
|
|
76
|
+
value = row.observed_usd.value
|
|
77
|
+
return float(value) if isinstance(value, (int, float)) else 0.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_alias_maps(result: Any) -> dict[str, dict[str, str]]:
|
|
81
|
+
every_row = list(result.contributors)
|
|
82
|
+
for class_result in result.classes:
|
|
83
|
+
for row in class_result.rows:
|
|
84
|
+
if row not in every_row:
|
|
85
|
+
every_row.append(row)
|
|
86
|
+
return {
|
|
87
|
+
"project": _alias_map(
|
|
88
|
+
[r for r in every_row if r.subject_kind == "project"], "project"),
|
|
89
|
+
"session": _alias_map(
|
|
90
|
+
[r for r in every_row if r.subject_kind == "session"], "session"),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _display_label(row: Any, aliases: Mapping[str, Mapping[str, str]],
|
|
95
|
+
*, reveal_projects: bool) -> str:
|
|
96
|
+
"""The label a person reads.
|
|
97
|
+
|
|
98
|
+
Projects are aliased by default. `--reveal-projects` exposes the derived
|
|
99
|
+
display label only — never a filesystem path, which is why the basename
|
|
100
|
+
is taken here rather than the raw key. Sessions stay opaque in EVERY
|
|
101
|
+
mode: a session identifier is not a name a person chose, and revealing
|
|
102
|
+
it buys nothing.
|
|
103
|
+
"""
|
|
104
|
+
if row.subject_kind == "project":
|
|
105
|
+
if not reveal_projects:
|
|
106
|
+
return aliases["project"].get(row.subject_key, row.subject_key)
|
|
107
|
+
raw = row.subject_label or row.subject_key
|
|
108
|
+
return raw.rstrip("/").rsplit("/", 1)[-1] or raw
|
|
109
|
+
if row.subject_kind == "session":
|
|
110
|
+
return aliases["session"].get(row.subject_key, row.subject_key)
|
|
111
|
+
return row.subject_label or row.subject_key
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _display_key(row: Any, aliases: Mapping[str, Mapping[str, str]]) -> str:
|
|
115
|
+
"""The key published on the wire.
|
|
116
|
+
|
|
117
|
+
A project bucket path and a session identity are both filesystem- or
|
|
118
|
+
account-revealing, so the published key is the alias in every mode; the
|
|
119
|
+
label is the only thing `--reveal-projects` widens.
|
|
120
|
+
"""
|
|
121
|
+
if row.subject_kind in ("project", "session"):
|
|
122
|
+
return aliases[row.subject_kind].get(row.subject_key, row.subject_key)
|
|
123
|
+
return row.subject_key
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --- the wire adapter ---------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _coverage_to_wire(coverage: Any) -> dict[str, Any] | None:
|
|
129
|
+
if coverage is None:
|
|
130
|
+
return None
|
|
131
|
+
payload: dict[str, Any] = {
|
|
132
|
+
"requestedStart": coverage.requested_start,
|
|
133
|
+
"requestedEnd": coverage.requested_end,
|
|
134
|
+
"observedStart": coverage.observed_start,
|
|
135
|
+
"observedEnd": coverage.observed_end,
|
|
136
|
+
"supportUnits": coverage.support_units,
|
|
137
|
+
"gapCodes": list(coverage.gap_codes),
|
|
138
|
+
}
|
|
139
|
+
# An unmeasurable dimension is ABSENT rather than zero. Zero is a
|
|
140
|
+
# measurement; absence is the statement that we do not know.
|
|
141
|
+
for wire, attribute in (
|
|
142
|
+
("countCoverage", "count_coverage"),
|
|
143
|
+
("usdCoverage", "usd_coverage"),
|
|
144
|
+
("identityCoverage", "identity_coverage"),
|
|
145
|
+
("retentionCoverage", "retention_coverage"),
|
|
146
|
+
("pricingCoverage", "pricing_coverage"),
|
|
147
|
+
# #620 S3. Both default to `None` and both are omitted when null, so
|
|
148
|
+
# every S2 coverage block stays byte-identical.
|
|
149
|
+
("evaluabilityCoverage", "evaluability_coverage"),
|
|
150
|
+
):
|
|
151
|
+
value = getattr(coverage, attribute)
|
|
152
|
+
if value is not None:
|
|
153
|
+
payload[wire] = value
|
|
154
|
+
dimensions = getattr(coverage, "dimensions", None)
|
|
155
|
+
if dimensions:
|
|
156
|
+
payload["dimensions"] = dict(dimensions)
|
|
157
|
+
return payload
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _evidence_to_wire(field: Any) -> dict[str, Any]:
|
|
161
|
+
payload: dict[str, Any] = {
|
|
162
|
+
"state": field.state,
|
|
163
|
+
"value": field.value,
|
|
164
|
+
"population": _coverage_to_wire(field.population),
|
|
165
|
+
"qualifications": list(field.qualifications),
|
|
166
|
+
}
|
|
167
|
+
if field.code is not None:
|
|
168
|
+
payload["code"] = field.code
|
|
169
|
+
return payload
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _row_to_wire(row: Any, aliases: Mapping[str, Mapping[str, str]], *,
|
|
173
|
+
reveal_projects: bool) -> dict[str, Any]:
|
|
174
|
+
payload: dict[str, Any] = {
|
|
175
|
+
"contributorClass": row.contributor_class,
|
|
176
|
+
"subjectKind": row.subject_kind,
|
|
177
|
+
"subjectKey": _display_key(row, aliases),
|
|
178
|
+
"subjectLabel": _display_label(row, aliases,
|
|
179
|
+
reveal_projects=reveal_projects),
|
|
180
|
+
"rank": row.rank,
|
|
181
|
+
"observedUsd": _evidence_to_wire(row.observed_usd),
|
|
182
|
+
"share": _evidence_to_wire(row.share),
|
|
183
|
+
"baseline": _evidence_to_wire(row.baseline),
|
|
184
|
+
"confidence": row.confidence,
|
|
185
|
+
"isFallbackPricing": row.is_fallback_pricing,
|
|
186
|
+
"nextStep": row.next_step,
|
|
187
|
+
}
|
|
188
|
+
# #620 S3. Emitted ONLY when non-empty. Publishing the key unconditionally
|
|
189
|
+
# would move every S2 row golden for no reason.
|
|
190
|
+
evidence = getattr(row, "evidence", None)
|
|
191
|
+
if evidence:
|
|
192
|
+
payload["evidence"] = {
|
|
193
|
+
name: _evidence_to_wire(fieldval)
|
|
194
|
+
for name, fieldval in evidence.items()
|
|
195
|
+
}
|
|
196
|
+
return payload
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _class_to_wire(class_result: Any, aliases, *, reveal_projects: bool):
|
|
200
|
+
return {
|
|
201
|
+
"contributorClass": class_result.contributor_class,
|
|
202
|
+
"verdict": class_result.verdict,
|
|
203
|
+
"code": class_result.code,
|
|
204
|
+
# Which minimum an `insufficient_population` class failed. `null` for
|
|
205
|
+
# every other verdict and for a class a provider-wide cause preempted,
|
|
206
|
+
# which never shaped subjects and so has no minimum to have failed.
|
|
207
|
+
"supportShortfall": class_result.support_shortfall,
|
|
208
|
+
# `null` for a class that measured nothing, which is NOT `low`:
|
|
209
|
+
# confidence is a statement about a measurement, and a withheld class
|
|
210
|
+
# made none. The client renders it rather than recomputing a server
|
|
211
|
+
# rule from the published constants.
|
|
212
|
+
"confidence": class_result.confidence,
|
|
213
|
+
"population": _coverage_to_wire(class_result.population),
|
|
214
|
+
"rows": [_row_to_wire(r, aliases, reveal_projects=reveal_projects)
|
|
215
|
+
for r in class_result.rows],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _generation_to_wire(result: Any, scope: Any) -> dict[str, Any] | None:
|
|
220
|
+
generation = result.generation
|
|
221
|
+
if generation is None:
|
|
222
|
+
return None
|
|
223
|
+
payload = dict(generation.as_dict())
|
|
224
|
+
# The plan is part of the identity (#620 S3 §3): without it a seven-class
|
|
225
|
+
# report with three withheld results could share an id with a four-class
|
|
226
|
+
# one. It is threaded on the result rather than hung off the scope,
|
|
227
|
+
# because `_scope_for` reconstructs scopes and would discard it.
|
|
228
|
+
plan = getattr(result, "plan", None)
|
|
229
|
+
if scope is not None and plan is not None:
|
|
230
|
+
payload["generationId"] = generation.generation_id(scope, plan)
|
|
231
|
+
return payload
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _result_to_wire(result: Any, scope: Any, *,
|
|
235
|
+
reveal_projects: bool) -> dict[str, Any]:
|
|
236
|
+
aliases = _build_alias_maps(result)
|
|
237
|
+
return {
|
|
238
|
+
"source": result.source,
|
|
239
|
+
"accountKey": result.account_key,
|
|
240
|
+
"effectiveSpeed": result.effective_speed,
|
|
241
|
+
"generation": _generation_to_wire(result, scope),
|
|
242
|
+
"denominator": {
|
|
243
|
+
"identity": result.denominator.identity,
|
|
244
|
+
"source": result.denominator.source,
|
|
245
|
+
"accountKey": result.denominator.account_key,
|
|
246
|
+
"windowStart": result.denominator.window_start,
|
|
247
|
+
"windowEnd": result.denominator.window_end,
|
|
248
|
+
"populationDigest": result.denominator.population_digest,
|
|
249
|
+
# An `EvidenceField`, not a bare float: a denominator withheld
|
|
250
|
+
# under the provider-wide cause ladder must say so on the wire
|
|
251
|
+
# too, or a client renders the same definite `$0.00` the terminal
|
|
252
|
+
# used to.
|
|
253
|
+
"usd": _evidence_to_wire(result.denominator.usd),
|
|
254
|
+
},
|
|
255
|
+
"coverage": _coverage_to_wire(result.coverage),
|
|
256
|
+
"verdict": result.verdict,
|
|
257
|
+
"code": result.code,
|
|
258
|
+
"applicableClassCount": result.applicable_class_count,
|
|
259
|
+
"withheldClassCount": result.withheld_class_count,
|
|
260
|
+
"contributors": [
|
|
261
|
+
_row_to_wire(r, aliases, reveal_projects=reveal_projects)
|
|
262
|
+
for r in result.contributors
|
|
263
|
+
],
|
|
264
|
+
"classes": [
|
|
265
|
+
_class_to_wire(c, aliases, reveal_projects=reveal_projects)
|
|
266
|
+
for c in result.classes
|
|
267
|
+
],
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _constants_to_wire() -> dict[str, Any]:
|
|
272
|
+
return {
|
|
273
|
+
"contributorShareFloor": kernel.DIAGNOSIS_CONTRIBUTOR_SHARE_FLOOR,
|
|
274
|
+
# The floor alone does not reproduce the verdict. A share is a ratio of
|
|
275
|
+
# two float sums, so a subject at exactly one fifth publishes as
|
|
276
|
+
# 0.19999999999999998, and a client applying `share >= floor` to that
|
|
277
|
+
# would compute `no_contributor` where the server published
|
|
278
|
+
# `contributor`. The published rule is `share >= floor - epsilon`, so
|
|
279
|
+
# both halves are published.
|
|
280
|
+
"shareFloorEpsilon": kernel.DIAGNOSIS_SHARE_FLOOR_EPSILON,
|
|
281
|
+
"tieEpsilonUsd": kernel.DIAGNOSIS_TIE_EPSILON_USD,
|
|
282
|
+
"confidenceHighMinSupport": kernel.CONFIDENCE_HIGH_MIN_SUPPORT,
|
|
283
|
+
"confidenceMediumMinSupport": kernel.CONFIDENCE_MEDIUM_MIN_SUPPORT,
|
|
284
|
+
"confidenceMediumMinCoverage": kernel.CONFIDENCE_MEDIUM_MIN_COVERAGE,
|
|
285
|
+
"withholdMinCoverage": kernel.WITHHOLD_MIN_COVERAGE,
|
|
286
|
+
"withholdMinSupport": kernel.WITHHOLD_MIN_SUPPORT,
|
|
287
|
+
# #620 S3 constants. Each decides a verdict, so each is published
|
|
288
|
+
# beside the rule that consumes it.
|
|
289
|
+
"shortConversationMaxHumanTurns":
|
|
290
|
+
kernel.DIAGNOSIS_SHORT_CONVERSATION_MAX_HUMAN_TURNS,
|
|
291
|
+
"largeContextMinWindowFraction":
|
|
292
|
+
kernel.DIAGNOSIS_LARGE_CONTEXT_MIN_WINDOW_FRACTION,
|
|
293
|
+
"minSubagentBuckets": kernel.DIAGNOSIS_MIN_SUBAGENT_BUCKETS,
|
|
294
|
+
"seedScanBudgetRows": kernel.DIAGNOSIS_SEED_SCAN_BUDGET_ROWS,
|
|
295
|
+
"conversationNormalizeBudgetRows":
|
|
296
|
+
kernel.DIAGNOSIS_CONVERSATION_NORMALIZE_BUDGET_ROWS,
|
|
297
|
+
"codexEventScanBudgetRows":
|
|
298
|
+
kernel.DIAGNOSIS_CODEX_EVENT_SCAN_BUDGET_ROWS,
|
|
299
|
+
"codexEventScanPerFileRows":
|
|
300
|
+
kernel.DIAGNOSIS_CODEX_EVENT_SCAN_PER_FILE_ROWS,
|
|
301
|
+
# The gap-code vocabulary, published the way `registry` publishes the
|
|
302
|
+
# class vocabulary. Without it a consumer receives `gapCodes` as bare
|
|
303
|
+
# strings with no way to tell a code from a newer server apart from
|
|
304
|
+
# one it should have handled, which makes the contract's claim that
|
|
305
|
+
# the set is closed on the server a claim no client can act on.
|
|
306
|
+
# Adding a member is additive evolution of a published enum, which
|
|
307
|
+
# `docs/cli-contract.md` permits.
|
|
308
|
+
"gapCodes": sorted(kernel.GAP_CODES),
|
|
309
|
+
# The turn predicate itself, so the two turn-based classes' rules are
|
|
310
|
+
# reproducible rather than merely readable.
|
|
311
|
+
"turnDefinition": kernel.DIAGNOSIS_TURN_DEFINITION_SENTENCE,
|
|
312
|
+
"registry": [_spec_to_wire(spec)
|
|
313
|
+
for spec in kernel.CONTRIBUTOR_REGISTRY],
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _spec_to_wire(spec: Any) -> dict[str, Any]:
|
|
318
|
+
payload: dict[str, Any] = {
|
|
319
|
+
"contributorClass": spec.kind,
|
|
320
|
+
"subjectKind": spec.subject_kind,
|
|
321
|
+
"label": spec.label,
|
|
322
|
+
"minDistinctSubjects": spec.min_distinct_subjects,
|
|
323
|
+
"minPricedEntries": spec.min_priced_entries,
|
|
324
|
+
}
|
|
325
|
+
# The published predicate. Emitted only when the spec carries one, so the
|
|
326
|
+
# four accounting classes keep their byte-frozen registry shape.
|
|
327
|
+
rule = getattr(spec, "rule", None)
|
|
328
|
+
if rule is not None:
|
|
329
|
+
payload["rule"] = {
|
|
330
|
+
"sentence": rule.sentence,
|
|
331
|
+
"parameters": dict(rule.parameters),
|
|
332
|
+
}
|
|
333
|
+
return payload
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def diagnosis_to_wire(report: Any, *, scopes: Mapping[str, Any] | None = None,
|
|
337
|
+
reveal_projects: bool = False) -> dict[str, Any]:
|
|
338
|
+
"""Serialize one report, stamped-first camelCase.
|
|
339
|
+
|
|
340
|
+
`scopes` maps a provider name to the `DiagnosisScope` that produced it, so
|
|
341
|
+
each result can publish its own `generationId`. It is optional because the
|
|
342
|
+
kernel-only tests build reports without a store.
|
|
343
|
+
"""
|
|
344
|
+
payload = {
|
|
345
|
+
"contractVersion": report.contract_version,
|
|
346
|
+
"measuredAt": report.measured_at,
|
|
347
|
+
"window": {
|
|
348
|
+
"startAt": report.window.start_at,
|
|
349
|
+
"endAt": report.window.end_at,
|
|
350
|
+
"tz": report.window.tz,
|
|
351
|
+
"label": report.window.label,
|
|
352
|
+
},
|
|
353
|
+
"constants": _constants_to_wire(),
|
|
354
|
+
"notes": {
|
|
355
|
+
"scope": kernel.DIAGNOSIS_SCOPE_SENTENCE,
|
|
356
|
+
"overlap": kernel.DIAGNOSIS_OVERLAP_SENTENCE,
|
|
357
|
+
},
|
|
358
|
+
"overallVerdict": report.overall_verdict,
|
|
359
|
+
"overallCode": report.overall_code,
|
|
360
|
+
# `contributor_detected` claims only that a contributor was found, so
|
|
361
|
+
# it can stand beside a withheld sibling class. These two make the
|
|
362
|
+
# incompleteness explicit, so the verdict can never be read as a
|
|
363
|
+
# complete account of the window.
|
|
364
|
+
"applicableClassCount": report.applicable_class_count,
|
|
365
|
+
"withheldClassCount": report.withheld_class_count,
|
|
366
|
+
"results": [
|
|
367
|
+
_result_to_wire(result, (scopes or {}).get(result.source),
|
|
368
|
+
reveal_projects=reveal_projects)
|
|
369
|
+
for result in report.results
|
|
370
|
+
],
|
|
371
|
+
}
|
|
372
|
+
return stamp_schema_version(payload, version=EXPLAIN_SCHEMA_VERSION)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _project(value: Any, path: str = "") -> Any:
|
|
376
|
+
if isinstance(value, dict):
|
|
377
|
+
projected = {}
|
|
378
|
+
for k, v in sorted(value.items()):
|
|
379
|
+
child = f"{path}.{k}" if path else k
|
|
380
|
+
if child in _EXCLUDED_PATHS:
|
|
381
|
+
continue
|
|
382
|
+
projected[k] = _project(v, child)
|
|
383
|
+
return projected
|
|
384
|
+
if isinstance(value, list):
|
|
385
|
+
return [_project(v, f"{path}[]") for v in value]
|
|
386
|
+
return value
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def canonical_projection(payload: Mapping[str, Any]) -> bytes:
|
|
390
|
+
"""The byte form the CLI and the route must agree on.
|
|
391
|
+
|
|
392
|
+
Equality is defined over this projection rather than over terminal text
|
|
393
|
+
against HTTP bytes. It names its exclusions — `measuredAt`, the header
|
|
394
|
+
notes and every presentation-only label — and fixes key ordering, so a
|
|
395
|
+
difference in the projection is a difference in the facts.
|
|
396
|
+
"""
|
|
397
|
+
return json.dumps(_project(dict(payload)), sort_keys=True,
|
|
398
|
+
separators=(",", ":")).encode()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# --- terminal rendering -------------------------------------------------
|
|
402
|
+
|
|
403
|
+
def _pct(value: float, places: int = 0) -> str:
|
|
404
|
+
"""A percentage rendered the way the client renders the same number.
|
|
405
|
+
|
|
406
|
+
Python's format spec rounds a tie to even and JavaScript's `toFixed`
|
|
407
|
+
rounds a tie away from zero, so one published `identityCoverage` of
|
|
408
|
+
exactly 0.125 printed `12%` here and `13%` in the modal. `Decimal(float)`
|
|
409
|
+
takes the exact binary value the client also holds, so ROUND_HALF_UP over
|
|
410
|
+
it reproduces `toFixed` for every non-negative percentage — including the
|
|
411
|
+
doubles that only look like ties, such as `0.145 * 100`, which both
|
|
412
|
+
surfaces render as 14.
|
|
413
|
+
|
|
414
|
+
This is rounding, not flooring, so the `math.floor(pct + 1e-9)` snap rule
|
|
415
|
+
does not apply: that rule exists to stop a fraction-times-100 losing a
|
|
416
|
+
whole percent to the last bit, and snapping here would round 12.4999… up.
|
|
417
|
+
"""
|
|
418
|
+
quantum = decimal.Decimal(1).scaleb(-places)
|
|
419
|
+
return str(decimal.Decimal(value * 100).quantize(
|
|
420
|
+
quantum, rounding=decimal.ROUND_HALF_UP))
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _fmt_usd(value: Any) -> str:
|
|
424
|
+
return f"${value:,.2f}" if isinstance(value, (int, float)) else "—"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _fmt_share(field: Any) -> str:
|
|
428
|
+
# The withheld branch is a deliberate renderer-side safety net, not live
|
|
429
|
+
# behaviour: `classify_class` gates the denominator before it builds any
|
|
430
|
+
# row, so no row S2 produces can carry a withheld share. It stays because
|
|
431
|
+
# a class added later that withholds one must print its cause rather than
|
|
432
|
+
# raise inside the report.
|
|
433
|
+
if field.state != "available" or not isinstance(field.value, (int, float)):
|
|
434
|
+
return f"withheld ({field.code})" if field.code else "withheld"
|
|
435
|
+
return f"{_pct(field.value, 1)}%"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _fmt_baseline(field: Any) -> str:
|
|
439
|
+
if field.state != "available" or not isinstance(field.value, (int, float)):
|
|
440
|
+
return f"withheld ({field.code})" if field.code else "withheld"
|
|
441
|
+
return f"{_pct(field.value, 1)}% previously"
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# How one evidence figure is rendered. Keyed by the published camelCase name,
|
|
445
|
+
# because the unit is a property of the FIGURE and not of its type: a float
|
|
446
|
+
# named `estWastedUsd` is dollars and a float named `largestSubagentShare` is
|
|
447
|
+
# a proportion, and rendering either as a bare repr makes the reader guess.
|
|
448
|
+
_EVIDENCE_USD_FIELDS = frozenset({"estWastedUsd", "unallocatedUsd"})
|
|
449
|
+
_EVIDENCE_SHARE_FIELDS = frozenset({"largestSubagentShare",
|
|
450
|
+
"maxContextWindowFraction"})
|
|
451
|
+
|
|
452
|
+
# The human label for one evidence figure. The published camelCase name is a
|
|
453
|
+
# WIRE vocabulary, and printing it on the terminal made this the one line in
|
|
454
|
+
# the report written in a different language from every other: the coverage
|
|
455
|
+
# line beside it says `cost coverage 100%` and the modal says `conversations
|
|
456
|
+
# affected`, while the terminal said `affectedConversationCount 1`.
|
|
457
|
+
#
|
|
458
|
+
# These strings are the same ones `evidenceLabel` renders in
|
|
459
|
+
# `dashboard/web/src/lib/diagnosis.ts`, so the two surfaces state one
|
|
460
|
+
# vocabulary. A key with no label here renders under its own published name
|
|
461
|
+
# rather than vanishing — the fallback is required, because a client reading a
|
|
462
|
+
# newer server must show the figure it cannot name.
|
|
463
|
+
#
|
|
464
|
+
# No label may contain a comma: the terminal joins these figures with `, `, so
|
|
465
|
+
# a comma inside a label makes the list ambiguous to read. The modal renders
|
|
466
|
+
# each figure in its own column and would not have shown the problem.
|
|
467
|
+
_EVIDENCE_LABELS: Mapping[str, str] = MappingProxyType({
|
|
468
|
+
"flaggedTurnCount": "turns that rebuilt their cache",
|
|
469
|
+
"affectedConversationCount": "conversations affected",
|
|
470
|
+
"estWastedUsd": "estimated wasted cost",
|
|
471
|
+
"conversationCount": "qualifying conversations",
|
|
472
|
+
"medianHumanTurns": "median human turns",
|
|
473
|
+
"maxContextWindowFraction":
|
|
474
|
+
"largest request as a share of its context window",
|
|
475
|
+
"identifiedSubagentCount": "identified subagents",
|
|
476
|
+
"largestSubagentShare": "largest subagent as a share of this class",
|
|
477
|
+
"unallocatedUsd": "unallocated cost",
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _evidence_label(name: str) -> str:
|
|
482
|
+
return _EVIDENCE_LABELS.get(name, name)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _fmt_evidence_value(name: str, value: Any) -> str:
|
|
486
|
+
if not isinstance(value, (int, float)):
|
|
487
|
+
return "—"
|
|
488
|
+
if name in _EVIDENCE_USD_FIELDS:
|
|
489
|
+
return _fmt_usd(value)
|
|
490
|
+
if name in _EVIDENCE_SHARE_FIELDS:
|
|
491
|
+
return f"{_pct(value, 1)}%"
|
|
492
|
+
return f"{value:,}" if isinstance(value, int) else repr(value)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _fmt_evidence_field(name: str, field: Any, *,
|
|
496
|
+
exclude: Sequence[str] = ()) -> str:
|
|
497
|
+
"""One evidence figure, VISIBLE rather than behind a hover.
|
|
498
|
+
|
|
499
|
+
A withheld member prints its cause, exactly as a withheld denominator
|
|
500
|
+
does: a row may report its cost while one member figure could not be
|
|
501
|
+
established, and printing nothing for that member would read as zero.
|
|
502
|
+
|
|
503
|
+
`exclude` drops the qualifications the ROW has already stated. A
|
|
504
|
+
qualification printed twice in one row reads as two separate facts about
|
|
505
|
+
two different figures; `identifiable_subset_unknown_completeness` appeared
|
|
506
|
+
once in the row's own parenthetical and again under `identified
|
|
507
|
+
subagents`, with the class rule beneath them stating the same thing in
|
|
508
|
+
English (#620 S3, browser round 1).
|
|
509
|
+
"""
|
|
510
|
+
if field.state != "available" or field.value is None:
|
|
511
|
+
rendered = f"withheld ({field.code})" if field.code else "withheld"
|
|
512
|
+
else:
|
|
513
|
+
rendered = _fmt_evidence_value(name, field.value)
|
|
514
|
+
stated = set(exclude)
|
|
515
|
+
qualifications = [mark for mark in getattr(field, "qualifications", ()) or ()
|
|
516
|
+
if mark not in stated]
|
|
517
|
+
if qualifications:
|
|
518
|
+
rendered += f" [{', '.join(qualifications)}]"
|
|
519
|
+
return f"{_evidence_label(name)} {rendered}"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _evidence_phrase(row: Any) -> str:
|
|
523
|
+
"""The class-specific evidence beneath one row, in published-name order.
|
|
524
|
+
|
|
525
|
+
Ordered by the PUBLISHED name and rendered under the human label, so the
|
|
526
|
+
line reads in the same order as the JSON and in the same words as the
|
|
527
|
+
modal. Ordering by the label instead would make a wording change reorder
|
|
528
|
+
the line and move a golden for a reason that is not the figure's.
|
|
529
|
+
|
|
530
|
+
The row's own qualifications are excluded from every member, because the
|
|
531
|
+
row prints them itself one line above — see `_fmt_evidence_field`. The
|
|
532
|
+
exclusion is derived from the row rather than passed in, so the terminal
|
|
533
|
+
and the modal cannot state the rule differently: the modal applies the
|
|
534
|
+
same one, over `observedUsd.qualifications`.
|
|
535
|
+
"""
|
|
536
|
+
evidence = getattr(row, "evidence", None) or {}
|
|
537
|
+
stated = getattr(getattr(row, "observed_usd", None), "qualifications",
|
|
538
|
+
()) or ()
|
|
539
|
+
return ", ".join(_fmt_evidence_field(name, evidence[name], exclude=stated)
|
|
540
|
+
for name in sorted(evidence))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _rule_lines() -> list[str]:
|
|
544
|
+
"""One line per class that publishes a rule, plus the turn predicate.
|
|
545
|
+
|
|
546
|
+
Rendered from `CONTRIBUTOR_REGISTRY` rather than written out, so a class
|
|
547
|
+
whose rule changes cannot leave the terminal stating the old one. The four
|
|
548
|
+
accounting classes carry no rule and contribute no line, which is what
|
|
549
|
+
keeps the S2 header byte-identical when nothing else changes it.
|
|
550
|
+
"""
|
|
551
|
+
lines: list[str] = []
|
|
552
|
+
for spec in kernel.CONTRIBUTOR_REGISTRY:
|
|
553
|
+
rule = getattr(spec, "rule", None)
|
|
554
|
+
if rule is None:
|
|
555
|
+
continue
|
|
556
|
+
lines.append(f" {spec.label}: {rule.sentence}")
|
|
557
|
+
if lines:
|
|
558
|
+
lines.append(f" {kernel.DIAGNOSIS_TURN_DEFINITION_SENTENCE}")
|
|
559
|
+
lines.insert(0, "Rules in force, by class:")
|
|
560
|
+
return lines
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _class_label(kind: str) -> str:
|
|
564
|
+
try:
|
|
565
|
+
return kernel.spec_for(kind).label
|
|
566
|
+
except KeyError:
|
|
567
|
+
return kind
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _fmt_denominator(field: Any) -> str:
|
|
571
|
+
"""The denominator line, which must never print a figure it does not have.
|
|
572
|
+
|
|
573
|
+
A withheld denominator prints its cause. The true retained cost of a
|
|
574
|
+
population nothing could price is unknown, not zero.
|
|
575
|
+
"""
|
|
576
|
+
if field.state != "available" or not isinstance(field.value, (int, float)):
|
|
577
|
+
cause = f"withheld ({field.code})" if field.code else "withheld"
|
|
578
|
+
# A withheld denominator may carry the failure's own message — the
|
|
579
|
+
# Codex quota projection names its remedy — and dropping it left the
|
|
580
|
+
# user with a cause and no next step.
|
|
581
|
+
detail = ", ".join(field.qualifications)
|
|
582
|
+
return f"{cause}: {detail}" if detail else cause
|
|
583
|
+
return f"{_fmt_usd(field.value)} of locally retained cost"
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _support_phrase(population: Any) -> str:
|
|
587
|
+
"""How many priced accounting entries a figure rests on.
|
|
588
|
+
|
|
589
|
+
`support_units` is `None` when the class never shaped a population at all,
|
|
590
|
+
which is not the same as shaping one and finding it empty, so the two
|
|
591
|
+
render differently.
|
|
592
|
+
"""
|
|
593
|
+
units = getattr(population, "support_units", 0)
|
|
594
|
+
if units is None:
|
|
595
|
+
return "support not measured"
|
|
596
|
+
return f"support {units} unit" if units == 1 else f"support {units} units"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
# The coverage dimensions a figure states, in the order they are read, each
|
|
600
|
+
# with the wording the dashboard's `CoverageNote` uses. One vocabulary across
|
|
601
|
+
# the two surfaces, so a reader moving between them is reading the same words
|
|
602
|
+
# about the same measurement.
|
|
603
|
+
_COVERAGE_DIMENSIONS: tuple[tuple[str, str], ...] = (
|
|
604
|
+
("usd_coverage", "cost coverage"),
|
|
605
|
+
("identity_coverage", "identity coverage"),
|
|
606
|
+
("pricing_coverage", "priced without fallback"),
|
|
607
|
+
("retention_coverage", "window retained"),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _gap_phrase(population: Any, exclude: Sequence[str] = ()) -> str:
|
|
612
|
+
"""WHY rows could not be decided, which is not why a field was withheld.
|
|
613
|
+
|
|
614
|
+
A class withheld because every spending session exhausted its seed share
|
|
615
|
+
printed its cause and nothing else, so the one fact that explains the
|
|
616
|
+
withholding was dropped exactly where the reader needs it. The cause
|
|
617
|
+
itself is excluded, because a preempted class carries it as its own first
|
|
618
|
+
gap code and printing it twice reads as two separate statements.
|
|
619
|
+
"""
|
|
620
|
+
codes = [code for code in getattr(population, "gap_codes", ()) or ()
|
|
621
|
+
if code not in set(exclude)]
|
|
622
|
+
return f"undecided: {', '.join(codes)}" if codes else ""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _publishes_gaps(kind: str) -> bool:
|
|
626
|
+
"""Whether this class states its undecided rows on this surface.
|
|
627
|
+
|
|
628
|
+
Derived from the published registry rather than from a second list of
|
|
629
|
+
class names: a spec carries a `rule` exactly when it is one of the three
|
|
630
|
+
conversation-derived classes, which are the ones whose predicate can fail
|
|
631
|
+
to decide a row. The four accounting classes keep their S2 wording, so no
|
|
632
|
+
S2 line on this surface moves for a change that is not theirs.
|
|
633
|
+
|
|
634
|
+
THE DERIVATION IS AN INFERENCE, and a reader changing the registry should
|
|
635
|
+
know which one. This function answers "does this class state its undecided
|
|
636
|
+
rows on the terminal" by asking "does this class publish a rule", and the
|
|
637
|
+
two are the same set only by today's construction. Giving an accounting
|
|
638
|
+
spec a `rule` — a reasonable thing to want, since the four accounting
|
|
639
|
+
predicates are perfectly statable — would silently add an `undecided:` line
|
|
640
|
+
to that class as well. That change is loud in the goldens, because it moves
|
|
641
|
+
a dozen of them; it is surprising in the source, because nothing at the
|
|
642
|
+
call site names gap lines. Left as an inference rather than a second list
|
|
643
|
+
on purpose: a hand-maintained list of class names is the failure this was
|
|
644
|
+
derived to avoid, and a wrong list fails silently where this fails in the
|
|
645
|
+
goldens.
|
|
646
|
+
"""
|
|
647
|
+
try:
|
|
648
|
+
return kernel.spec_for(kind).rule is not None
|
|
649
|
+
except KeyError:
|
|
650
|
+
return False
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _coverage_phrase(population: Any, *, gaps: bool = False) -> str:
|
|
654
|
+
"""The population one measured figure rests on.
|
|
655
|
+
|
|
656
|
+
Each dimension is rendered ONLY when it is present. An absent dimension is
|
|
657
|
+
"not measured", and printing `0%` for it would state a measurement nobody
|
|
658
|
+
made — a figure rendered over a population that did not support it.
|
|
659
|
+
"""
|
|
660
|
+
parts = [_support_phrase(population)]
|
|
661
|
+
for attribute, label in _COVERAGE_DIMENSIONS:
|
|
662
|
+
value = getattr(population, attribute, None)
|
|
663
|
+
if value is not None:
|
|
664
|
+
parts.append(f"{label} {_pct(value)}%")
|
|
665
|
+
# The only dimension that falls when a row could not be DECIDED, which is
|
|
666
|
+
# a different statement from an identity that did not resolve. Published
|
|
667
|
+
# by the three conversation-derived classes only, so no S2 line moves.
|
|
668
|
+
evaluability = getattr(population, "evaluability_coverage", None)
|
|
669
|
+
if evaluability is not None:
|
|
670
|
+
parts.append(f"decidable {_pct(evaluability)}%")
|
|
671
|
+
gap_phrase = _gap_phrase(population) if gaps else ""
|
|
672
|
+
if gap_phrase:
|
|
673
|
+
parts.append(gap_phrase)
|
|
674
|
+
return ", ".join(parts)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _class_support(class_result: Any) -> str:
|
|
678
|
+
"""The population and the confidence of one MEASURED class.
|
|
679
|
+
|
|
680
|
+
A class admitted at 0.50 dollar coverage can report `no_contributor` at
|
|
681
|
+
`low` confidence, and a bare class label said nothing about either.
|
|
682
|
+
|
|
683
|
+
Both facts, in the wording the dashboard's `CoverageNote` uses, because
|
|
684
|
+
the terminal previously stated confidence and no coverage dimensions
|
|
685
|
+
while the modal stated the dimensions and no confidence: a reader moving
|
|
686
|
+
between the two surfaces saw each fact on only one of them.
|
|
687
|
+
"""
|
|
688
|
+
confidence = class_result.confidence or kernel.assess_confidence(
|
|
689
|
+
class_result.population)
|
|
690
|
+
phrase = _coverage_phrase(
|
|
691
|
+
class_result.population,
|
|
692
|
+
gaps=_publishes_gaps(class_result.contributor_class))
|
|
693
|
+
return f"{phrase}, confidence {confidence}"
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _shortfall_phrase(class_result: Any) -> str:
|
|
697
|
+
"""Which support minimum an `insufficient_population` class failed."""
|
|
698
|
+
code = getattr(class_result, "support_shortfall", None)
|
|
699
|
+
if code is None:
|
|
700
|
+
return ""
|
|
701
|
+
try:
|
|
702
|
+
spec = kernel.spec_for(class_result.contributor_class)
|
|
703
|
+
except KeyError:
|
|
704
|
+
return code
|
|
705
|
+
if code == kernel.SUPPORT_SHORTFALL_DISTINCT_SUBJECTS:
|
|
706
|
+
return (f"fewer than {spec.min_distinct_subjects} distinct "
|
|
707
|
+
f"{spec.subject_kind}s")
|
|
708
|
+
if code == kernel.SUPPORT_SHORTFALL_PRICED_ENTRIES:
|
|
709
|
+
return f"fewer than {spec.min_priced_entries} priced entries"
|
|
710
|
+
if code == kernel.SUPPORT_SHORTFALL_USD_COVERAGE:
|
|
711
|
+
return f"dollar coverage below {kernel.WITHHOLD_MIN_COVERAGE:.2f}"
|
|
712
|
+
if code == kernel.SUPPORT_SHORTFALL_NO_PRICED_DOLLARS:
|
|
713
|
+
return "no priced dollars to divide by"
|
|
714
|
+
return code
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _withheld_note(class_result: Any) -> str:
|
|
718
|
+
"""What a class with no measurement can honestly say about its population.
|
|
719
|
+
|
|
720
|
+
Confidence is a statement ABOUT a measurement, and a withheld class made
|
|
721
|
+
none, so it is not printed here: `insufficient_population (support 60
|
|
722
|
+
units, confidence high)` claimed high confidence in an answer that does
|
|
723
|
+
not exist. What is printed instead is the shortfall that actually applies,
|
|
724
|
+
because `insufficient_population` is one cause for four different
|
|
725
|
+
shortfalls and the support count belongs to only one of them.
|
|
726
|
+
"""
|
|
727
|
+
parts = [_support_phrase(class_result.population)]
|
|
728
|
+
shortfall = _shortfall_phrase(class_result)
|
|
729
|
+
if shortfall:
|
|
730
|
+
parts.append(shortfall)
|
|
731
|
+
# A class withheld because every spending session exhausted its seed share
|
|
732
|
+
# must still say `scan_budget_exhausted` beside the cause, or the reader is
|
|
733
|
+
# left with a cause and no reason. The cause itself is excluded: a
|
|
734
|
+
# preempted class carries it as its own first gap code.
|
|
735
|
+
if _publishes_gaps(class_result.contributor_class):
|
|
736
|
+
gaps = _gap_phrase(class_result.population,
|
|
737
|
+
exclude=(class_result.code,) if class_result.code
|
|
738
|
+
else ())
|
|
739
|
+
if gaps:
|
|
740
|
+
parts.append(gaps)
|
|
741
|
+
return ", ".join(parts)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def render_terminal(report: Any, *, reveal_projects: bool = False) -> str:
|
|
745
|
+
floor = kernel.DIAGNOSIS_CONTRIBUTOR_SHARE_FLOOR
|
|
746
|
+
lines: list[str] = []
|
|
747
|
+
lines.append(
|
|
748
|
+
f"Window {report.window.start_at} .. {report.window.end_at} "
|
|
749
|
+
f"[{report.window.tz}] (half-open: the start is included, the end is "
|
|
750
|
+
f"not)"
|
|
751
|
+
)
|
|
752
|
+
lines.append(f"Measured at {report.measured_at}")
|
|
753
|
+
lines.append(kernel.DIAGNOSIS_SCOPE_SENTENCE)
|
|
754
|
+
lines.append(kernel.DIAGNOSIS_OVERLAP_SENTENCE)
|
|
755
|
+
lines.append(
|
|
756
|
+
f"A subject is reported as a contributor at a share of {floor:.2f} "
|
|
757
|
+
f"or more of its named denominator."
|
|
758
|
+
)
|
|
759
|
+
# The published predicate of every class that has one. A verdict a reader
|
|
760
|
+
# cannot reproduce is a verdict they have to trust.
|
|
761
|
+
rule_lines = _rule_lines()
|
|
762
|
+
if rule_lines:
|
|
763
|
+
lines.append("")
|
|
764
|
+
lines.extend(rule_lines)
|
|
765
|
+
|
|
766
|
+
for result in report.results:
|
|
767
|
+
aliases = _build_alias_maps(result)
|
|
768
|
+
account = result.account_key or "all accounts"
|
|
769
|
+
lines.append("")
|
|
770
|
+
lines.append(f"== {result.source} · {account} ==")
|
|
771
|
+
lines.append(
|
|
772
|
+
f"Denominator {result.denominator.identity}: "
|
|
773
|
+
f"{_fmt_denominator(result.denominator.usd)}"
|
|
774
|
+
)
|
|
775
|
+
if result.effective_speed:
|
|
776
|
+
lines.append(f"Effective speed: {result.effective_speed}")
|
|
777
|
+
|
|
778
|
+
if result.contributors:
|
|
779
|
+
lines.append("")
|
|
780
|
+
lines.append("Reported contributors, ranked by observed cost:")
|
|
781
|
+
for row in result.contributors:
|
|
782
|
+
label = _display_label(row, aliases,
|
|
783
|
+
reveal_projects=reveal_projects)
|
|
784
|
+
notes = [f"confidence {row.confidence}"]
|
|
785
|
+
if row.is_fallback_pricing:
|
|
786
|
+
notes.append("fallback pricing")
|
|
787
|
+
notes.extend(row.observed_usd.qualifications)
|
|
788
|
+
lines.append(
|
|
789
|
+
f" {row.rank}. [{_class_label(row.contributor_class)}] "
|
|
790
|
+
f"{label} — {_fmt_usd(row.observed_usd.value)}, "
|
|
791
|
+
f"{_fmt_share(row.share)} of the denominator "
|
|
792
|
+
f"({', '.join(notes)})"
|
|
793
|
+
)
|
|
794
|
+
lines.append(f" baseline: {_fmt_baseline(row.baseline)}")
|
|
795
|
+
# The population this row's figures rest on. Every class line
|
|
796
|
+
# below states its own; a contributor row that stated none was
|
|
797
|
+
# the one figure on this surface a reader could not weigh.
|
|
798
|
+
lines.append(
|
|
799
|
+
f" coverage: "
|
|
800
|
+
f"{_coverage_phrase(row.observed_usd.population, gaps=_publishes_gaps(row.contributor_class))}"
|
|
801
|
+
)
|
|
802
|
+
# Class-specific evidence, VISIBLE on the row rather than
|
|
803
|
+
# behind a hover (R9). Absent for every S2 class, whose
|
|
804
|
+
# evidence mapping is empty, so no S2 row gains a line.
|
|
805
|
+
evidence = _evidence_phrase(row)
|
|
806
|
+
if evidence:
|
|
807
|
+
lines.append(f" evidence: {evidence}")
|
|
808
|
+
lines.append(f" -> Run {row.next_step}")
|
|
809
|
+
|
|
810
|
+
clean = [c for c in result.classes
|
|
811
|
+
if c.verdict == kernel.VerdictState.NO_CONTRIBUTOR.value]
|
|
812
|
+
if clean:
|
|
813
|
+
lines.append("")
|
|
814
|
+
lines.append("Classes reporting no contributor:")
|
|
815
|
+
for class_result in clean:
|
|
816
|
+
lines.append(
|
|
817
|
+
f" {_class_label(class_result.contributor_class)} "
|
|
818
|
+
f"({_class_support(class_result)})"
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
withheld = [c for c in result.classes
|
|
822
|
+
if c.verdict == kernel.VerdictState.WITHHELD.value]
|
|
823
|
+
if withheld:
|
|
824
|
+
lines.append("")
|
|
825
|
+
lines.append("Classes with no measurement to report:")
|
|
826
|
+
for class_result in withheld:
|
|
827
|
+
cause = class_result.code or class_result.verdict
|
|
828
|
+
lines.append(
|
|
829
|
+
f" {_class_label(class_result.contributor_class)} — {cause}"
|
|
830
|
+
f" ({_withheld_note(class_result)})"
|
|
831
|
+
)
|
|
832
|
+
# Spec 5.4. On a denied dashboard the same class name renders
|
|
833
|
+
# two verdicts in one report, and a reader who sees one
|
|
834
|
+
# withheld and one measured must be told why rather than left
|
|
835
|
+
# to infer a bug. Unreachable from this surface — the CLI
|
|
836
|
+
# reads its own stores and is always authorized — and stated
|
|
837
|
+
# here so both surfaces carry one sentence rather than two.
|
|
838
|
+
if (class_result.contributor_class == "subagent_fanout"
|
|
839
|
+
and cause == kernel.WithheldCause
|
|
840
|
+
.TRANSCRIPTS_NOT_VISIBLE.value):
|
|
841
|
+
lines.append(
|
|
842
|
+
f" {kernel.DIAGNOSIS_TRANSCRIPT_ASYMMETRY_SENTENCE}"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
# `not_applicable` is NOT a withheld measurement: it says the provider
|
|
846
|
+
# structurally cannot support the class, and it is excluded from the
|
|
847
|
+
# completeness test for that reason. Filing it under the withheld
|
|
848
|
+
# heading contradicts the distinction the contract rests on. No S2
|
|
849
|
+
# class reaches this, and S3's cache-churn class does.
|
|
850
|
+
inapplicable = [c for c in result.classes
|
|
851
|
+
if c.verdict == kernel.VerdictState.NOT_APPLICABLE.value]
|
|
852
|
+
if inapplicable:
|
|
853
|
+
lines.append("")
|
|
854
|
+
lines.append("Classes this provider does not support:")
|
|
855
|
+
for class_result in inapplicable:
|
|
856
|
+
lines.append(
|
|
857
|
+
f" {_class_label(class_result.contributor_class)} — "
|
|
858
|
+
f"not applicable to {result.source}"
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
lines.append("")
|
|
862
|
+
verdict = result.verdict + (f" ({result.code})" if result.code else "")
|
|
863
|
+
lines.append(
|
|
864
|
+
f"Verdict for {result.source}: {verdict} "
|
|
865
|
+
f"[{result.withheld_class_count} of "
|
|
866
|
+
f"{result.applicable_class_count} applicable classes withheld]"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
lines.append("")
|
|
870
|
+
overall = report.overall_verdict + (
|
|
871
|
+
f" ({report.overall_code})" if report.overall_code else ""
|
|
872
|
+
)
|
|
873
|
+
# A `contributor_detected` verdict claims only that a contributor was
|
|
874
|
+
# found, so it must never be read as a complete account of the window.
|
|
875
|
+
lines.append(
|
|
876
|
+
f"Overall: {overall} "
|
|
877
|
+
f"[{report.withheld_class_count} of "
|
|
878
|
+
f"{report.applicable_class_count} applicable classes withheld]"
|
|
879
|
+
)
|
|
880
|
+
return "\n".join(lines) + "\n"
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
# --- the command --------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
@dataclass(frozen=True)
|
|
886
|
+
class _ExactWindow:
|
|
887
|
+
"""The parsed form of `--start-at` / `--end-at`.
|
|
888
|
+
|
|
889
|
+
It carries the same three attributes `_parse_diff_window` returns, so the
|
|
890
|
+
caller reads one shape whichever grammar produced it.
|
|
891
|
+
"""
|
|
892
|
+
|
|
893
|
+
start_utc: dt.datetime
|
|
894
|
+
end_utc: dt.datetime
|
|
895
|
+
label: str = ""
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _parse_exact_bound(raw: str, flag: str) -> dt.datetime:
|
|
899
|
+
"""One exact instant, in UTC.
|
|
900
|
+
|
|
901
|
+
A DATE-ONLY value is refused rather than read as midnight somewhere. The
|
|
902
|
+
same refusal governs `five-hour-breakdown --block-start`, and for the same
|
|
903
|
+
reason: a date is not an instant, and choosing a zone on the user's behalf
|
|
904
|
+
measures a window nobody asked for. A full-ISO form carries its own offset
|
|
905
|
+
and is timezone-independent; a naive datetime is read as UTC, which is the
|
|
906
|
+
convention `--block-start` already sets.
|
|
907
|
+
"""
|
|
908
|
+
text = (raw or "").strip()
|
|
909
|
+
if "T" not in text and " " not in text:
|
|
910
|
+
raise ValueError(
|
|
911
|
+
f"{flag} must be an ISO instant such as 2026-08-10T00:00:00Z, "
|
|
912
|
+
f"not the date {text!r}"
|
|
913
|
+
)
|
|
914
|
+
try:
|
|
915
|
+
parsed = dt.datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
916
|
+
except ValueError as exc:
|
|
917
|
+
raise ValueError(f"{flag} must be an ISO instant: {exc}") from exc
|
|
918
|
+
if parsed.tzinfo is None:
|
|
919
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
920
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _resolve_window(args, now_utc: dt.datetime, tz_name: str):
|
|
924
|
+
"""Resolve the window token through the shared kernel grammar.
|
|
925
|
+
|
|
926
|
+
`_parse_diff_window` already sits in `bin/_lib_diff_kernel.py` and is
|
|
927
|
+
already re-exported, so `explain` reuses it in place rather than moving
|
|
928
|
+
it (E4 was dropped for exactly that reason).
|
|
929
|
+
|
|
930
|
+
Exact bounds bypass that grammar entirely. They are already validated as
|
|
931
|
+
a pair by `cmd_explain`, so reaching here with only one of them is not
|
|
932
|
+
possible.
|
|
933
|
+
"""
|
|
934
|
+
c = _cctally()
|
|
935
|
+
dk = c._load_sibling("_lib_diff_kernel")
|
|
936
|
+
start_raw = getattr(args, "start_at", None)
|
|
937
|
+
end_raw = getattr(args, "end_at", None)
|
|
938
|
+
if start_raw or end_raw:
|
|
939
|
+
start_at = _parse_exact_bound(start_raw, "--start-at")
|
|
940
|
+
end_at = _parse_exact_bound(end_raw, "--end-at")
|
|
941
|
+
if end_at <= start_at:
|
|
942
|
+
# A selector error, so exit 2 — the same code a malformed
|
|
943
|
+
# `--window` token gets. `DiagnosisScope` would raise
|
|
944
|
+
# `range_unresolved` for it instead, which is exit 3 and would
|
|
945
|
+
# report a wrong argument as an infrastructure failure.
|
|
946
|
+
raise ValueError("--end-at must be after --start-at")
|
|
947
|
+
return _ExactWindow(start_utc=start_at, end_utc=end_at)
|
|
948
|
+
token = getattr(args, "window", None) or "this-week"
|
|
949
|
+
try:
|
|
950
|
+
# The anchor read goes through the ORDINARY stats opener, which
|
|
951
|
+
# migrates, repairs and prints. An absolute window does not need it,
|
|
952
|
+
# so it is resolved only when the grammar says the token cannot be
|
|
953
|
+
# resolved without it — which is exactly what NoAnchorError reports.
|
|
954
|
+
return dk._parse_diff_window(
|
|
955
|
+
token, now_utc=now_utc, anchor_resets_at=None,
|
|
956
|
+
anchor_week_start=None, tz_name=tz_name,
|
|
957
|
+
)
|
|
958
|
+
except dk.NoAnchorError:
|
|
959
|
+
pass
|
|
960
|
+
try:
|
|
961
|
+
anchor_week_start, anchor_resets_at = dk._diff_resolve_anchor(now_utc)
|
|
962
|
+
except (sqlite3.Error, ValueError, TypeError, AttributeError) as exc:
|
|
963
|
+
# The anchor read is a STORE read, so its failures belong to the
|
|
964
|
+
# establishment taxonomy, not to selector validation. Reporting a
|
|
965
|
+
# malformed stats.db as `explain: database disk image is malformed`
|
|
966
|
+
# with exit 2 told the user to fix a `--window` token that was never
|
|
967
|
+
# wrong, and hid an infrastructure failure inside the argument rules.
|
|
968
|
+
#
|
|
969
|
+
# The tuple is deliberately wider than the two SQLite-shaped causes,
|
|
970
|
+
# and the two type errors are store signals rather than code bugs.
|
|
971
|
+
# `_diff_resolve_anchor` calls `str.replace` on the stored timestamp
|
|
972
|
+
# with no type guard, and stats.db is a non-STRICT SQLite database, so
|
|
973
|
+
# the declared TEXT type is an affinity rather than a constraint: a
|
|
974
|
+
# BLOB in that column is returned as `bytes` and raises `TypeError`.
|
|
975
|
+
# (An INTEGER cannot reach it — TEXT affinity converts one to text on
|
|
976
|
+
# insert — so `AttributeError` is named for the same class of store
|
|
977
|
+
# damage rather than because this schema can produce it.) Neither was
|
|
978
|
+
# caught, so the exception escaped into the CLI's top-level handler
|
|
979
|
+
# and printed `Error: a bytes-like object is required, not 'str'` at
|
|
980
|
+
# exit 1, which spec §4 does not use. `open_db()` failures never reach
|
|
981
|
+
# here — the anchor helper catches those itself and returns
|
|
982
|
+
# `(None, None)`. The widening stays bounded to this one call: a
|
|
983
|
+
# `TypeError` from anywhere else in the command still propagates,
|
|
984
|
+
# because reporting a code bug as an infrastructure failure tells the
|
|
985
|
+
# user to fix a store that is not broken.
|
|
986
|
+
raise kernel.EstablishmentFailure(
|
|
987
|
+
kernel.EstablishmentError.STORE_UNAVAILABLE.value,
|
|
988
|
+
f"the subscription-week anchor could not be read: {exc}",
|
|
989
|
+
) from exc
|
|
990
|
+
return dk._parse_diff_window(
|
|
991
|
+
token, now_utc=now_utc,
|
|
992
|
+
anchor_resets_at=anchor_resets_at,
|
|
993
|
+
anchor_week_start=anchor_week_start,
|
|
994
|
+
tz_name=tz_name,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def resolve_tz_name(args, config) -> str:
|
|
999
|
+
"""The display zone both surfaces resolve, in one place.
|
|
1000
|
+
|
|
1001
|
+
`display_tz` is part of the scope the generation id is computed over and is
|
|
1002
|
+
published as `window.tz`, which the canonical projection keeps. So the CLI
|
|
1003
|
+
and the route agreeing about it is not a nicety: two surfaces reading the
|
|
1004
|
+
same facts under two spellings of the same zone publish two different
|
|
1005
|
+
`generationId` values, and the equivalence check then compares two
|
|
1006
|
+
different questions.
|
|
1007
|
+
"""
|
|
1008
|
+
c = _cctally()
|
|
1009
|
+
tz_obj = c.resolve_display_tz(args, config)
|
|
1010
|
+
return tz_obj.key if tz_obj is not None else c._local_tz_name()
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _resolve_account_key(sources, args, provider: str):
|
|
1014
|
+
"""Resolve `--account` over the diagnosis's own read-only open path.
|
|
1015
|
+
|
|
1016
|
+
`resolve_account_filter` reaches stats.db through the ordinary opener,
|
|
1017
|
+
which migrates, repairs, imports and replays — the same class of problem
|
|
1018
|
+
the week anchor already avoids. `explain` reads and never writes, so it
|
|
1019
|
+
resolves the ref itself over `mode=ro`, and only when a ref was actually
|
|
1020
|
+
supplied.
|
|
1021
|
+
"""
|
|
1022
|
+
ref = getattr(args, "account", None)
|
|
1023
|
+
if ref is None:
|
|
1024
|
+
return None, None
|
|
1025
|
+
import _lib_accounts as accounts
|
|
1026
|
+
|
|
1027
|
+
c = _cctally()
|
|
1028
|
+
try:
|
|
1029
|
+
conn = sources.open_read_only("stats")
|
|
1030
|
+
except kernel.EstablishmentFailure:
|
|
1031
|
+
# Selector validation is exit 2, and a ref that cannot be resolved is
|
|
1032
|
+
# unresolvable whatever the reason: with no account registry on the
|
|
1033
|
+
# machine there is no account this ref could name. Letting the failure
|
|
1034
|
+
# through turned an exit-2 selector error into an exit-3 establishment
|
|
1035
|
+
# error on any install without a stats.db.
|
|
1036
|
+
print(f"account: --account {ref!r} is ambiguous or unknown "
|
|
1037
|
+
"(this machine holds no account registry)", file=sys.stderr)
|
|
1038
|
+
return None, 2
|
|
1039
|
+
try:
|
|
1040
|
+
try:
|
|
1041
|
+
return accounts.resolve_account_ref(conn, ref, provider), None
|
|
1042
|
+
except accounts.AccountRefError as exc:
|
|
1043
|
+
print(f"account: --account {ref!r} is ambiguous or unknown",
|
|
1044
|
+
file=sys.stderr)
|
|
1045
|
+
c._load_sibling("_cctally_account").print_ref_candidates(
|
|
1046
|
+
conn, exc.candidates
|
|
1047
|
+
)
|
|
1048
|
+
return None, 2
|
|
1049
|
+
finally:
|
|
1050
|
+
conn.close()
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def cmd_explain(args) -> int:
|
|
1054
|
+
c = _cctally()
|
|
1055
|
+
sources = _sources()
|
|
1056
|
+
source = getattr(args, "source", "claude") or "claude"
|
|
1057
|
+
account = getattr(args, "account", None)
|
|
1058
|
+
|
|
1059
|
+
# Account keys are provider-scoped, so one selector cannot address both
|
|
1060
|
+
# providers. This mirrors the established behaviour recorded at
|
|
1061
|
+
# docs/commands/account.md:94.
|
|
1062
|
+
if account is not None and source == "all":
|
|
1063
|
+
print("explain: --account cannot be combined with --source all "
|
|
1064
|
+
"(account keys are provider-scoped)", file=sys.stderr)
|
|
1065
|
+
return 2
|
|
1066
|
+
|
|
1067
|
+
# The two window grammars are mutually exclusive, and both bounds of the
|
|
1068
|
+
# exact one are required together. Validated HERE rather than through an
|
|
1069
|
+
# argparse mutually-exclusive group, because `--window` carries a default
|
|
1070
|
+
# and argparse cannot tell a default apart from an explicit value.
|
|
1071
|
+
window_token = getattr(args, "window", None)
|
|
1072
|
+
start_raw = getattr(args, "start_at", None)
|
|
1073
|
+
end_raw = getattr(args, "end_at", None)
|
|
1074
|
+
if (start_raw or end_raw) and window_token is not None:
|
|
1075
|
+
print("explain: --start-at/--end-at cannot be combined with --window "
|
|
1076
|
+
"(a window token names calendar days; exact bounds name "
|
|
1077
|
+
"instants)", file=sys.stderr)
|
|
1078
|
+
return 2
|
|
1079
|
+
if bool(start_raw) != bool(end_raw):
|
|
1080
|
+
print("explain: --start-at and --end-at must be given together",
|
|
1081
|
+
file=sys.stderr)
|
|
1082
|
+
return 2
|
|
1083
|
+
|
|
1084
|
+
now_utc = _command_as_of()
|
|
1085
|
+
config = c.load_config()
|
|
1086
|
+
tz_name = resolve_tz_name(args, config)
|
|
1087
|
+
|
|
1088
|
+
dk = c._load_sibling("_lib_diff_kernel")
|
|
1089
|
+
try:
|
|
1090
|
+
parsed = _resolve_window(args, now_utc, tz_name)
|
|
1091
|
+
except (ValueError, dk.NoAnchorError) as exc:
|
|
1092
|
+
# Exactly the two argument-shaped failures: a token the grammar cannot
|
|
1093
|
+
# parse, and one that needs a week anchor this machine cannot supply.
|
|
1094
|
+
# A bare `except Exception` here also swallowed store failures raised
|
|
1095
|
+
# by the anchor read and reported them as malformed selectors.
|
|
1096
|
+
print(f"explain: {exc}", file=sys.stderr)
|
|
1097
|
+
return 2
|
|
1098
|
+
except kernel.EstablishmentFailure as exc:
|
|
1099
|
+
print(f"explain: {exc.code}: {exc.message}", file=sys.stderr)
|
|
1100
|
+
return 3
|
|
1101
|
+
|
|
1102
|
+
try:
|
|
1103
|
+
account_key, account_exit = _resolve_account_key(
|
|
1104
|
+
sources, args, source if source in ("claude", "codex") else "claude",
|
|
1105
|
+
)
|
|
1106
|
+
except kernel.EstablishmentFailure as exc:
|
|
1107
|
+
print(f"explain: {exc.code}: {exc.message}", file=sys.stderr)
|
|
1108
|
+
return 3
|
|
1109
|
+
if account_exit is not None:
|
|
1110
|
+
return account_exit
|
|
1111
|
+
|
|
1112
|
+
speed = getattr(args, "speed", None)
|
|
1113
|
+
if speed == "auto":
|
|
1114
|
+
speed = None
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
scope = sources.DiagnosisScope(
|
|
1118
|
+
source=source,
|
|
1119
|
+
account_key=account_key,
|
|
1120
|
+
window_start=parsed.start_utc,
|
|
1121
|
+
window_end=parsed.end_utc,
|
|
1122
|
+
effective_speed=speed,
|
|
1123
|
+
display_tz=tz_name,
|
|
1124
|
+
label=parsed.label,
|
|
1125
|
+
)
|
|
1126
|
+
# The CLI reads this machine's own stores directly, so there is no
|
|
1127
|
+
# authorization layer to compose and transcripts are always visible.
|
|
1128
|
+
# Stated rather than defaulted: `build_diagnosis` carries no default,
|
|
1129
|
+
# because the one call site that forgot it was the dashboard route.
|
|
1130
|
+
report = sources.build_diagnosis(scope, measured_at=now_utc,
|
|
1131
|
+
transcripts_visible=True)
|
|
1132
|
+
except kernel.EstablishmentFailure as exc:
|
|
1133
|
+
# An `EstablishmentFailure` is always exit 3. Argument-shaped
|
|
1134
|
+
# validation — a malformed `--window` token, an unresolvable account
|
|
1135
|
+
# ref — is exit 2 and fails at argument-parse time above, never by
|
|
1136
|
+
# becoming an establishment failure here.
|
|
1137
|
+
print(f"explain: {exc.code}: {exc.message}", file=sys.stderr)
|
|
1138
|
+
return 3
|
|
1139
|
+
|
|
1140
|
+
reveal = bool(getattr(args, "reveal_projects", False))
|
|
1141
|
+
scopes = {result.source: _scope_for(sources, scope, result.source)
|
|
1142
|
+
for result in report.results}
|
|
1143
|
+
# A store that could not be read is an infrastructure failure rather than
|
|
1144
|
+
# an answer about data availability, so when no requested provider
|
|
1145
|
+
# answered, the report is still PRINTED — a person needs to see the typed
|
|
1146
|
+
# cause — and the command still exits 3, so a script sees the failure. A
|
|
1147
|
+
# report withheld for any other cause exits 0, because "the store does not
|
|
1148
|
+
# hold that window" is a correct answer.
|
|
1149
|
+
exit_code = 3 if kernel.unreadable_store_is_terminal(report) else 0
|
|
1150
|
+
if getattr(args, "emit_json", False) or getattr(args, "json", False):
|
|
1151
|
+
payload = diagnosis_to_wire(report, scopes=scopes,
|
|
1152
|
+
reveal_projects=reveal)
|
|
1153
|
+
print(json.dumps(payload, indent=2))
|
|
1154
|
+
return exit_code
|
|
1155
|
+
|
|
1156
|
+
sys.stdout.write(render_terminal(report, reveal_projects=reveal))
|
|
1157
|
+
return exit_code
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _scope_for(sources, scope, source: str):
|
|
1161
|
+
"""The per-provider scope `build_diagnosis` used for one result."""
|
|
1162
|
+
if scope.source == source:
|
|
1163
|
+
return scope
|
|
1164
|
+
return sources.DiagnosisScope(
|
|
1165
|
+
source=source,
|
|
1166
|
+
account_key=scope.account_key,
|
|
1167
|
+
window_start=scope.window_start,
|
|
1168
|
+
window_end=scope.window_end,
|
|
1169
|
+
effective_speed=scope.effective_speed if source == "codex" else None,
|
|
1170
|
+
display_tz=scope.display_tz,
|
|
1171
|
+
label=scope.label,
|
|
1172
|
+
)
|