cctally 1.69.1 → 1.69.3

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.
@@ -0,0 +1,511 @@
1
+ """Immutable, privacy-safe source dashboard contracts for #294 S4."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import datetime as dt
6
+ import hashlib
7
+ import json
8
+ import math
9
+ import re
10
+ import sqlite3
11
+ from dataclasses import dataclass
12
+ from types import MappingProxyType
13
+ from typing import Literal, Mapping
14
+
15
+
16
+ PhysicalSource = Literal["claude", "codex"]
17
+ DashboardSelection = Literal["claude", "codex", "all"]
18
+ Availability = Literal["ok", "empty", "partial", "unavailable"]
19
+ Freshness = Literal["fresh", "stale"]
20
+ CapabilityStatus = Literal[
21
+ "supported", "derived", "unavailable", "deferred", "not_applicable",
22
+ ]
23
+
24
+ SOURCE_SCHEMA_VERSION = 1
25
+ DEFAULT_SOURCE = "claude"
26
+ SOURCE_ORDER = ("claude", "codex", "all")
27
+
28
+ _PHYSICAL_SOURCES = frozenset(("claude", "codex"))
29
+ _SELECTIONS = frozenset(SOURCE_ORDER)
30
+ _AVAILABILITY = frozenset(("ok", "empty", "partial", "unavailable"))
31
+ _FRESHNESS = frozenset(("fresh", "stale"))
32
+ _CAPABILITY_STATUSES = frozenset((
33
+ "supported", "derived", "unavailable", "deferred", "not_applicable",
34
+ ))
35
+ _RESOURCE_RE = re.compile(r"[a-z][a-z0-9_]*\Z")
36
+
37
+
38
+ def _nonempty_string(value: object, name: str) -> str:
39
+ if not isinstance(value, str) or not value:
40
+ raise ValueError(f"{name} must be a non-empty string")
41
+ return value
42
+
43
+
44
+ def validate_physical_source(source: object) -> PhysicalSource:
45
+ """Validate one storage provider; ``all`` never reaches physical layers."""
46
+ if source not in _PHYSICAL_SOURCES:
47
+ raise ValueError("source must be one of ['claude', 'codex']")
48
+ return source # type: ignore[return-value]
49
+
50
+
51
+ def validate_dashboard_selection(source: object) -> DashboardSelection:
52
+ """Validate a dashboard presentation selection, including ``all``."""
53
+ if source not in _SELECTIONS:
54
+ raise ValueError("source must be one of ['all', 'claude', 'codex']")
55
+ return source # type: ignore[return-value]
56
+
57
+
58
+ def _freeze(value: object) -> object:
59
+ """Recursively freeze the published, request-thread-readable value tree."""
60
+ if type(value) is MappingProxyType:
61
+ return value
62
+ if isinstance(value, Mapping):
63
+ return MappingProxyType({str(key): _freeze(item) for key, item in value.items()})
64
+ if isinstance(value, (tuple, list)):
65
+ return tuple(_freeze(item) for item in value)
66
+ if isinstance(value, (set, frozenset)):
67
+ return frozenset(_freeze(item) for item in value)
68
+ return value
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class SourceDashboardWarning:
73
+ """A stable, public-safe provider degradation diagnostic."""
74
+
75
+ code: str
76
+ message: str
77
+ domain: str | None = None
78
+
79
+ def __post_init__(self) -> None:
80
+ _nonempty_string(self.code, "code")
81
+ _nonempty_string(self.message, "message")
82
+ if self.domain is not None:
83
+ _nonempty_string(self.domain, "domain")
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class CapabilityRecord:
88
+ """Descriptive support state, deliberately not an ambiguous boolean."""
89
+
90
+ status: CapabilityStatus
91
+ semantics: str | None = None
92
+
93
+ def __post_init__(self) -> None:
94
+ if self.status not in _CAPABILITY_STATUSES:
95
+ raise ValueError("unsupported capability status")
96
+ if self.semantics is not None:
97
+ _nonempty_string(self.semantics, "semantics")
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class SourceDashboardState:
102
+ """One atomically-published source read model."""
103
+
104
+ source: DashboardSelection
105
+ availability: Availability
106
+ freshness: Freshness
107
+ warnings: tuple[SourceDashboardWarning, ...]
108
+ data_version: str
109
+ last_success_at: dt.datetime | None
110
+ capabilities: Mapping[str, CapabilityRecord]
111
+ data: Mapping[str, object] | None
112
+ # Immutable, server-only facts used to advance an idle presentation clock.
113
+ # They are deliberately separate from ``data`` so no internal accounting
114
+ # evidence becomes part of the public source-envelope contract.
115
+ clock_data: Mapping[str, object] | None = None
116
+
117
+ def __post_init__(self) -> None:
118
+ validate_dashboard_selection(self.source)
119
+ if self.availability not in _AVAILABILITY:
120
+ raise ValueError("unsupported availability")
121
+ if self.freshness not in _FRESHNESS:
122
+ raise ValueError("unsupported freshness")
123
+ if not isinstance(self.data_version, str):
124
+ raise ValueError("data_version must be a string")
125
+ if self.availability != "unavailable":
126
+ _nonempty_string(self.data_version, "data_version")
127
+ if self.last_success_at is not None:
128
+ if self.last_success_at.tzinfo is None or self.last_success_at.utcoffset() is None:
129
+ raise ValueError("last_success_at must be timezone-aware")
130
+ object.__setattr__(
131
+ self, "last_success_at", self.last_success_at.astimezone(dt.timezone.utc),
132
+ )
133
+ warnings = tuple(self.warnings)
134
+ if not all(isinstance(item, SourceDashboardWarning) for item in warnings):
135
+ raise ValueError("warnings must contain SourceDashboardWarning values")
136
+ capabilities = {
137
+ _nonempty_string(name, "capability name"): value
138
+ for name, value in self.capabilities.items()
139
+ }
140
+ if not all(isinstance(value, CapabilityRecord) for value in capabilities.values()):
141
+ raise ValueError("capabilities must contain CapabilityRecord values")
142
+ object.__setattr__(self, "warnings", warnings)
143
+ object.__setattr__(self, "capabilities", _freeze(capabilities))
144
+ if self.data is not None:
145
+ object.__setattr__(self, "data", _freeze(self.data))
146
+ if self.clock_data is not None:
147
+ object.__setattr__(self, "clock_data", _freeze(self.clock_data))
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class SourceDashboardBundle:
152
+ """The complete source state published once with a dashboard snapshot."""
153
+
154
+ source_schema_version: int
155
+ default_source: DashboardSelection
156
+ source_order: tuple[DashboardSelection, ...]
157
+ sources: Mapping[DashboardSelection, SourceDashboardState]
158
+
159
+ def __post_init__(self) -> None:
160
+ if self.source_schema_version != SOURCE_SCHEMA_VERSION:
161
+ raise ValueError("unsupported source schema version")
162
+ if self.default_source != DEFAULT_SOURCE:
163
+ raise ValueError("default source must be claude")
164
+ if tuple(self.source_order) != SOURCE_ORDER:
165
+ raise ValueError("source order must be ('claude', 'codex', 'all')")
166
+ sources = dict(self.sources)
167
+ if set(sources) != set(SOURCE_ORDER):
168
+ raise ValueError("sources must contain exactly claude, codex, and all")
169
+ for source, state in sources.items():
170
+ validate_dashboard_selection(source)
171
+ if not isinstance(state, SourceDashboardState) or state.source != source:
172
+ raise ValueError("source state must match its source key")
173
+ object.__setattr__(self, "sources", _freeze(sources))
174
+
175
+
176
+ def _typed_identity_part(value: object) -> object:
177
+ """Return an unambiguous, canonical JSON-safe identity fragment."""
178
+ if value is None:
179
+ return ["null", None]
180
+ if isinstance(value, bool):
181
+ return ["bool", value]
182
+ if isinstance(value, int):
183
+ return ["int", value]
184
+ if isinstance(value, float):
185
+ if not math.isfinite(value):
186
+ raise ValueError("identity float must be finite")
187
+ return ["float", value.hex()]
188
+ if isinstance(value, str):
189
+ return ["str", _nonempty_string(value, "identity part")]
190
+ if isinstance(value, (tuple, list)):
191
+ return ["sequence", [_typed_identity_part(item) for item in value]]
192
+ raise ValueError("identity parts must be typed JSON scalar or sequence values")
193
+
194
+
195
+ def dashboard_resource_key(resource: object, source: object, *identity_parts: object) -> str:
196
+ """Build a non-reversible, provider-qualified resource identifier.
197
+
198
+ The digest covers typed values, so e.g. ``1`` cannot collide with ``"1"``.
199
+ Raw roots, native IDs, and compound identity values are never encoded in
200
+ the returned key.
201
+ """
202
+ kind = _nonempty_string(resource, "resource")
203
+ if not _RESOURCE_RE.fullmatch(kind):
204
+ raise ValueError("resource must use lowercase snake-case")
205
+ provider = validate_physical_source(source)
206
+ if not identity_parts:
207
+ raise ValueError("at least one identity part is required")
208
+ canonical = json.dumps(
209
+ {
210
+ "identity": [_typed_identity_part(part) for part in identity_parts],
211
+ "resource": kind,
212
+ "source": provider,
213
+ "version": 1,
214
+ },
215
+ ensure_ascii=False,
216
+ separators=(",", ":"),
217
+ sort_keys=True,
218
+ ).encode("utf-8")
219
+ digest = hashlib.sha256(b"cctally-dashboard-resource-v1\0" + canonical).digest()
220
+ token = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
221
+ return f"{kind}:{token}"
222
+
223
+
224
+ def degrade_source_state(
225
+ prior: SourceDashboardState,
226
+ warning: SourceDashboardWarning,
227
+ ) -> SourceDashboardState:
228
+ """Retain one complete prior source generation during a transient failure."""
229
+ if not isinstance(prior, SourceDashboardState):
230
+ raise ValueError("prior must be a SourceDashboardState")
231
+ if not isinstance(warning, SourceDashboardWarning):
232
+ raise ValueError("warning must be a SourceDashboardWarning")
233
+ # There must be a coherent prior generation to retain. An unavailable prior
234
+ # carries no data and an empty ``data_version``; degrading it to ``partial``
235
+ # would build an invalid state (the non-empty-data_version invariant only
236
+ # exempts ``unavailable``) and raise. Stay unavailable, carrying the new
237
+ # warning — this is the 2nd+ consecutive failing sync of a degraded provider.
238
+ if prior.availability == "unavailable" or not prior.data_version:
239
+ return unavailable_source_state(prior.source, warning)
240
+ return SourceDashboardState(
241
+ source=prior.source,
242
+ availability="partial",
243
+ freshness="stale",
244
+ warnings=(warning,),
245
+ data_version=prior.data_version,
246
+ last_success_at=prior.last_success_at,
247
+ capabilities=prior.capabilities,
248
+ data=prior.data,
249
+ clock_data=prior.clock_data,
250
+ )
251
+
252
+
253
+ def unavailable_source_state(
254
+ source: PhysicalSource,
255
+ warning: SourceDashboardWarning,
256
+ ) -> SourceDashboardState:
257
+ """Return a safe failure state when no coherent generation exists yet."""
258
+ validate_physical_source(source)
259
+ if not isinstance(warning, SourceDashboardWarning):
260
+ raise ValueError("warning must be a SourceDashboardWarning")
261
+ return SourceDashboardState(
262
+ source=source,
263
+ availability="unavailable",
264
+ freshness="stale",
265
+ warnings=(warning,),
266
+ data_version="",
267
+ last_success_at=None,
268
+ capabilities={},
269
+ data=None,
270
+ )
271
+
272
+
273
+ def _coherent_provider(state: SourceDashboardState) -> bool:
274
+ return (
275
+ state.availability in ("ok", "empty")
276
+ and state.freshness == "fresh"
277
+ and state.data is not None
278
+ )
279
+
280
+
281
+ def reuse_coherent_source_state(
282
+ prior: SourceDashboardState | None,
283
+ *,
284
+ data_version: str,
285
+ ) -> SourceDashboardState | None:
286
+ """Return the exact prior object only for an unchanged coherent source.
287
+
288
+ A stale/partial object deliberately does not qualify: the next coherent
289
+ rebuild must construct a replacement so it clears the transient warning
290
+ rather than preserving an old degraded generation indefinitely.
291
+ """
292
+ if prior is None:
293
+ return None
294
+ if not isinstance(prior, SourceDashboardState):
295
+ raise ValueError("prior must be a SourceDashboardState or None")
296
+ return prior if _coherent_provider(prior) and prior.data_version == data_version else None
297
+
298
+
299
+ def _combined_metrics(
300
+ claude: SourceDashboardState,
301
+ codex: SourceDashboardState,
302
+ ) -> Mapping[str, object] | None:
303
+ if not (_coherent_provider(claude) and _coherent_provider(codex)):
304
+ return None
305
+ try:
306
+ claude_hero = claude.data["hero"]
307
+ codex_hero = codex.data["hero"]
308
+ if not isinstance(claude_hero, Mapping) or not isinstance(codex_hero, Mapping):
309
+ return None
310
+ claude_cost = claude_hero["cost_usd"]
311
+ codex_cost = codex_hero["cost_usd"]
312
+ claude_tokens = claude_hero["total_tokens"]
313
+ codex_tokens = codex_hero["total_tokens"]
314
+ if (
315
+ isinstance(claude_cost, bool) or not isinstance(claude_cost, (int, float))
316
+ or isinstance(codex_cost, bool) or not isinstance(codex_cost, (int, float))
317
+ or isinstance(claude_tokens, bool) or not isinstance(claude_tokens, int)
318
+ or isinstance(codex_tokens, bool) or not isinstance(codex_tokens, int)
319
+ ):
320
+ return None
321
+ except (KeyError, TypeError):
322
+ return None
323
+ return {
324
+ "cost_usd": float(claude_cost) + float(codex_cost),
325
+ "total_tokens": claude_tokens + codex_tokens,
326
+ }
327
+
328
+
329
+ def _combined_alert_rows(
330
+ claude: SourceDashboardState,
331
+ codex: SourceDashboardState,
332
+ ) -> tuple[Mapping[str, object], ...]:
333
+ """Merge only provider-owned public alert rows with stable tie breaking."""
334
+ ordered: list[Mapping[str, object]] = []
335
+ for source, state in (("claude", claude), ("codex", codex)):
336
+ if not isinstance(state.data, Mapping):
337
+ continue
338
+ alerts = state.data.get("alerts")
339
+ rows = alerts.get("rows") if isinstance(alerts, Mapping) else None
340
+ if not isinstance(rows, (tuple, list)):
341
+ continue
342
+ for row in rows:
343
+ if not isinstance(row, Mapping) or row.get("source") != source:
344
+ continue
345
+ ordered.append(row)
346
+ # Python's stable sort preserves declared source order, then each source's
347
+ # native order, when alert timestamps tie.
348
+ return tuple(sorted(
349
+ ordered,
350
+ key=lambda row: str(row.get("created_at") or ""),
351
+ reverse=True,
352
+ ))
353
+
354
+
355
+ def compose_all_state(
356
+ claude: SourceDashboardState,
357
+ codex: SourceDashboardState,
358
+ ) -> SourceDashboardState:
359
+ """Compose provider-labeled sections without inventing blended semantics."""
360
+ if claude.source != "claude" or codex.source != "codex":
361
+ raise ValueError("all composition requires Claude and Codex provider states")
362
+ combined = _combined_metrics(claude, codex)
363
+ coherent = combined is not None
364
+ if coherent:
365
+ availability: Availability = (
366
+ "empty"
367
+ if claude.availability == "empty" and codex.availability == "empty"
368
+ else "ok"
369
+ )
370
+ freshness: Freshness = "fresh"
371
+ else:
372
+ availability = "partial"
373
+ freshness = "stale"
374
+ version_material = json.dumps(
375
+ [claude.data_version, codex.data_version, coherent],
376
+ separators=(",", ":"),
377
+ ).encode("utf-8")
378
+ data_version = "all:" + hashlib.sha256(version_material).hexdigest()[:24]
379
+ successes = (item for item in (claude.last_success_at, codex.last_success_at) if item is not None)
380
+ last_success_at = min(successes, default=None)
381
+ return SourceDashboardState(
382
+ source="all",
383
+ availability=availability,
384
+ freshness=freshness,
385
+ warnings=tuple((*claude.warnings, *codex.warnings)),
386
+ data_version=data_version,
387
+ last_success_at=last_success_at,
388
+ capabilities={
389
+ "hero": CapabilityRecord("derived", "compatible-provider-totals"),
390
+ "quota": CapabilityRecord("not_applicable", "provider-native"),
391
+ "budget": CapabilityRecord("not_applicable", "provider-native"),
392
+ "alerts": CapabilityRecord("derived", "provider-native-union"),
393
+ },
394
+ data={
395
+ "combined": combined,
396
+ "alerts": {"rows": _combined_alert_rows(claude, codex)},
397
+ "providers": {
398
+ "claude": claude.data,
399
+ "codex": codex.data,
400
+ },
401
+ },
402
+ )
403
+
404
+
405
+ @dataclass(frozen=True)
406
+ class ProjectionCoherence:
407
+ """Typed result for a Codex physical-to-projection coherence check."""
408
+
409
+ coherent: bool
410
+ reason: str | None = None
411
+
412
+
413
+ # The column order is the approved cross-database identity contract. Keep the
414
+ # relation sequence and tuples fixed: neither SQLite insertion order nor
415
+ # surrogate/provenance/reconciliation-only fields may perturb the digest.
416
+ _CODEX_STATS_DIGEST_RELATIONS: tuple[tuple[str, str], ...] = (
417
+ (
418
+ "quota_projection_state",
419
+ "SELECT source_root_key, physical_signature "
420
+ "FROM quota_projection_state "
421
+ "ORDER BY source_root_key, physical_signature",
422
+ ),
423
+ (
424
+ "quota_window_blocks",
425
+ "SELECT source, source_root_key, logical_limit_key, observed_slot, "
426
+ "window_minutes, limit_id, limit_name, resets_at_utc, nominal_start_at_utc, "
427
+ "first_observed_at_utc, last_observed_at_utc, first_percent, current_percent, "
428
+ "orphaned_at FROM quota_window_blocks WHERE source='codex' "
429
+ "ORDER BY source, source_root_key, logical_limit_key, observed_slot, "
430
+ "window_minutes, limit_id, limit_name, resets_at_utc, nominal_start_at_utc, "
431
+ "first_observed_at_utc, last_observed_at_utc, first_percent, current_percent, orphaned_at",
432
+ ),
433
+ (
434
+ "quota_percent_milestones",
435
+ "SELECT source, source_root_key, logical_limit_key, observed_slot, "
436
+ "window_minutes, resets_at_utc, percent_threshold, captured_at_utc, "
437
+ "high_water_percent, orphaned_at FROM quota_percent_milestones "
438
+ "WHERE source='codex' ORDER BY source, source_root_key, logical_limit_key, "
439
+ "observed_slot, window_minutes, resets_at_utc, percent_threshold, captured_at_utc, "
440
+ "high_water_percent, orphaned_at",
441
+ ),
442
+ (
443
+ "quota_threshold_events",
444
+ "SELECT source, source_root_key, logical_limit_key, observed_slot, "
445
+ "window_minutes, resets_at_utc, threshold, qualifying_kind, qualifying_percent, "
446
+ "projected_percent, severity, created_at_utc, disposition, alerted_at, suppressed_at, "
447
+ "orphaned_at FROM quota_threshold_events WHERE source='codex' "
448
+ "ORDER BY source, source_root_key, logical_limit_key, observed_slot, window_minutes, "
449
+ "resets_at_utc, threshold, qualifying_kind, qualifying_percent, projected_percent, "
450
+ "severity, created_at_utc, disposition, alerted_at, suppressed_at, orphaned_at",
451
+ ),
452
+ (
453
+ "budget_milestones",
454
+ "SELECT vendor, period_start_at, period, threshold, budget_usd, spent_usd, "
455
+ "consumption_pct, crossed_at_utc, alerted_at FROM budget_milestones "
456
+ "WHERE vendor='codex' ORDER BY vendor, period_start_at, period, threshold, "
457
+ "budget_usd, spent_usd, consumption_pct, crossed_at_utc, alerted_at",
458
+ ),
459
+ (
460
+ "projected_milestones",
461
+ "SELECT week_start_at, period, metric, threshold, projected_value, denominator, "
462
+ "crossed_at_utc, alerted_at FROM projected_milestones "
463
+ "WHERE metric='codex_budget_usd' ORDER BY week_start_at, period, metric, threshold, "
464
+ "projected_value, denominator, crossed_at_utc, alerted_at",
465
+ ),
466
+ )
467
+
468
+
469
+ def codex_stats_digest(stats_conn: sqlite3.Connection) -> str:
470
+ """Hash exact, canonically ordered Codex-derived stats relations.
471
+
472
+ A missing table is an empty relation so an older/fresh stats database has a
473
+ stable digest. Other SQLite failures remain visible to the builder, which
474
+ then follows the source all-or-prior failure matrix instead of publishing a
475
+ guessed identity.
476
+ """
477
+ relations: list[list[list[object]]] = []
478
+ for _name, query in _CODEX_STATS_DIGEST_RELATIONS:
479
+ try:
480
+ rows = stats_conn.execute(query).fetchall()
481
+ except sqlite3.OperationalError as exc:
482
+ if "no such table" not in str(exc).lower():
483
+ raise
484
+ rows = ()
485
+ relations.append([list(row) for row in rows])
486
+ canonical = json.dumps(
487
+ relations,
488
+ allow_nan=False,
489
+ ensure_ascii=False,
490
+ separators=(",", ":"),
491
+ ).encode("utf-8")
492
+ return hashlib.sha256(canonical).hexdigest()
493
+
494
+
495
+ def assess_codex_projection_coherence(
496
+ *,
497
+ active_root_keys: tuple[str, ...] | list[str] | set[str],
498
+ physical_signatures: Mapping[str, str],
499
+ projection_signatures: Mapping[str, str],
500
+ ) -> ProjectionCoherence:
501
+ """Require a complete, exact physical-signature match for every root."""
502
+ for root_key in sorted(active_root_keys):
503
+ physical = physical_signatures.get(root_key)
504
+ if physical is None:
505
+ return ProjectionCoherence(False, "missing_physical_signature")
506
+ projection = projection_signatures.get(root_key)
507
+ if projection is None:
508
+ return ProjectionCoherence(False, "missing_projection_state")
509
+ if physical != projection:
510
+ return ProjectionCoherence(False, "physical_signature_mismatch")
511
+ return ProjectionCoherence(True)