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.
Files changed (53) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +8 -2
  3. package/bin/_cctally_alerts.py +13 -2
  4. package/bin/_cctally_cache.py +3 -1
  5. package/bin/_cctally_cache_report.py +103 -6
  6. package/bin/_cctally_dashboard.py +1140 -282
  7. package/bin/_cctally_dashboard_conversation.py +12 -0
  8. package/bin/_cctally_dashboard_envelope.py +53 -61
  9. package/bin/_cctally_dashboard_share.py +101 -29
  10. package/bin/_cctally_dashboard_sources.py +663 -192
  11. package/bin/_cctally_diagnosis.py +1172 -0
  12. package/bin/_cctally_diagnosis_sources.py +4054 -0
  13. package/bin/_cctally_diff.py +20 -0
  14. package/bin/_cctally_forecast.py +329 -111
  15. package/bin/_cctally_milestone_history.py +10 -2
  16. package/bin/_cctally_parser.py +84 -0
  17. package/bin/_cctally_project.py +155 -47
  18. package/bin/_cctally_quota.py +14 -0
  19. package/bin/_cctally_record.py +151 -71
  20. package/bin/_cctally_refresh.py +105 -93
  21. package/bin/_cctally_share.py +9 -2
  22. package/bin/_cctally_source_analytics.py +40 -4
  23. package/bin/_cctally_statusline.py +8 -1
  24. package/bin/_cctally_tui.py +425 -234
  25. package/bin/_lib_alert_scope.py +685 -0
  26. package/bin/_lib_alerts_payload.py +112 -7
  27. package/bin/_lib_blocks.py +12 -0
  28. package/bin/_lib_cache_report.py +110 -1
  29. package/bin/_lib_codex_conversation.py +14 -0
  30. package/bin/_lib_codex_conversation_query.py +22 -8
  31. package/bin/_lib_codex_pools.py +20 -8
  32. package/bin/_lib_conversation.py +6 -3
  33. package/bin/_lib_conversation_query.py +256 -69
  34. package/bin/_lib_dashboard_sources.py +212 -24
  35. package/bin/_lib_diagnosis.py +1261 -0
  36. package/bin/_lib_forecast.py +62 -4
  37. package/bin/_lib_perf.py +12 -0
  38. package/bin/_lib_pricing.py +8 -7
  39. package/bin/_lib_readme_refresh.py +26 -5
  40. package/bin/_lib_render.py +31 -3
  41. package/bin/_lib_share_templates.py +150 -55
  42. package/bin/_lib_snapshot_cache.py +71 -13
  43. package/bin/_lib_source_identity.py +50 -2
  44. package/bin/_lib_subscription_weeks.py +65 -0
  45. package/bin/cctally +103 -16
  46. package/bin/cctally-explain +5 -0
  47. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
  48. package/dashboard/static/assets/index-Di2hljvB.css +1 -0
  49. package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
  50. package/dashboard/static/dashboard.html +2 -2
  51. package/package.json +6 -1
  52. package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
  53. 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
+ )