cctally 1.117.2 → 1.118.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 (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -0,0 +1,831 @@
1
+ """The trigger-maintained content revision of stats.db's Claude domain (#1050).
2
+
3
+ An in-place rewrite of a Claude-published stats row (a Model-A convergence, a
4
+ reviewed rederive, the open-block projection, a probe ``UPDATE``) moves none of
5
+ the append-shaped legs the dashboard compares (`MAX(id)`, `_reset_sig`, the
6
+ armed-row digests). This kernel gives that domain one identity
7
+ (spec `docs/superpowers/specs/2026-11-04-1050-correction-signal-design.md`
8
+ §4.1–§4.4):
9
+
10
+ * ``stats_content_revision`` — a singleton row carrying the kernel
11
+ ``format_version``, the ``domain_generation`` an installation repair bumps,
12
+ and ``seq``, the global sequence every tracked mutation advances;
13
+ * ``stats_content_revision_scopes`` — per ``(kind, account_key, scope_key)``
14
+ the ``seq`` of the latest tracked mutation that dirtied that scope, with the
15
+ epoch range a ``block`` scope spans.
16
+
17
+ Triggers on the §4.2 tables do the bookkeeping inside the writer's own
18
+ statement: the singleton is advanced FIRST, then each dirtied scope is
19
+ upserted with ``revision`` = the new ``seq``. A rolled-back write rolls its
20
+ trigger effects back with it, and an assignment of identical values fires
21
+ nothing (every ``WHEN`` compares null-safely with ``IS NOT``). The revision
22
+ tables carry no triggers and ``recursive_triggers`` stays off.
23
+
24
+ Installation never bumps the stats epoch. ``classify`` reads only (one bounded
25
+ singleton read plus one ``sqlite_schema`` read), so a ``current`` open writes
26
+ and locks nothing; ``ensure_stats_content_revision`` installs an ``absent``
27
+ store and repairs a ``partial`` one under the open-time maintenance guard with
28
+ a zero wait, and leaves the store untouched on contention.
29
+
30
+ ``read_claude_content_revision`` returns the opaque equality token of the
31
+ Claude content domain within stats.db, including its publication generation
32
+ (the #790 contract): the token changes on every tracked mutation, on every
33
+ repair and on every publication, and is ``""`` only for a never-mutated,
34
+ never-repaired, never-published store, so a fresh store stays byte-neutral.
35
+
36
+ Stdlib only, and no sibling import statement (core imports this module, so it
37
+ must stay a proven leaf). The guard and its contention error are reached at
38
+ call time, and only when an install is needed.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import hashlib
43
+ import importlib
44
+ import json
45
+ import re
46
+ import secrets
47
+ import sqlite3
48
+ import types
49
+ from typing import Mapping, NamedTuple, Sequence
50
+
51
+ #: The kernel's object format. A store whose singleton names a higher format is
52
+ #: ``unsupported`` (left alone); a lower one is ``partial`` (repaired in place).
53
+ KERNEL_FORMAT_VERSION: int = 1
54
+
55
+ STATE_CURRENT = "current"
56
+ STATE_ABSENT = "absent"
57
+ STATE_PARTIAL = "partial"
58
+ STATE_UNSUPPORTED = "unsupported"
59
+ STATE_MALFORMED = "malformed"
60
+ STATES = frozenset({
61
+ STATE_CURRENT, STATE_ABSENT, STATE_PARTIAL, STATE_UNSUPPORTED,
62
+ STATE_MALFORMED,
63
+ })
64
+
65
+ REVISION_TABLE = "stats_content_revision"
66
+ SCOPE_TABLE = "stats_content_revision_scopes"
67
+ USAGE_SNAPSHOT_INDEX = "idx_five_hour_milestones_usage_snapshot_id"
68
+ TRIGGER_PREFIX = "trg_content_revision_"
69
+
70
+ SCOPE_KINDS = ("cohort", "bucket", "block", "reset_point")
71
+
72
+ REVISION_TABLE_SQL = (
73
+ "CREATE TABLE stats_content_revision ("
74
+ "singleton INTEGER PRIMARY KEY CHECK (singleton = 1), "
75
+ "format_version INTEGER NOT NULL CHECK (format_version >= 1), "
76
+ "domain_generation INTEGER NOT NULL DEFAULT 0 "
77
+ "CHECK (domain_generation >= 0), "
78
+ "seq INTEGER NOT NULL DEFAULT 0 CHECK (seq >= 0))"
79
+ )
80
+ SCOPE_TABLE_SQL = (
81
+ "CREATE TABLE stats_content_revision_scopes ("
82
+ "kind TEXT NOT NULL "
83
+ "CHECK (kind IN ('cohort', 'bucket', 'block', 'reset_point')), "
84
+ "account_key TEXT NOT NULL, "
85
+ "scope_key TEXT NOT NULL, "
86
+ "start_epoch INTEGER, "
87
+ "end_epoch INTEGER, "
88
+ "revision INTEGER NOT NULL, "
89
+ "PRIMARY KEY (kind, account_key, scope_key))"
90
+ )
91
+ #: §4.2: every trigger lookup is a primary-key or indexed equality, and the
92
+ #: snapshot → five-hour milestone reference had no index of its own.
93
+ USAGE_SNAPSHOT_INDEX_SQL = (
94
+ "CREATE INDEX idx_five_hour_milestones_usage_snapshot_id "
95
+ "ON five_hour_milestones(usage_snapshot_id)"
96
+ )
97
+
98
+
99
+ class ContentRevision(NamedTuple):
100
+ """One read of the identity. ``token``/``cycle_basis`` are ``None`` in
101
+ every untrusted state (``absent``, ``partial``, ``unsupported``)."""
102
+
103
+ state: str
104
+ token: "str | None"
105
+ cycle_basis: "str | None"
106
+
107
+
108
+ class StatsContentRevisionUnreadable(Exception):
109
+ """The identity cannot be read: a ``malformed`` store, or more than one
110
+ ``stats_publication_stamp`` row. Callers treat it like any other failed
111
+ dispatch read (a nonce leg), never as an empty identity."""
112
+
113
+
114
+ class TableClassification(NamedTuple):
115
+ """One §4.2 row. ``compared`` columns fire the UPDATE trigger when they
116
+ differ null-safely; ``excluded`` columns are bookkeeping and never do.
117
+ ``insert_rule``: ``"tracked"`` (every INSERT), ``"below_max"`` (only an
118
+ INSERT below the table's ``MAX(id)``), ``"exempt"`` (another leg already
119
+ covers INSERT) or ``"untracked"``. ``scopes``: the scope kinds dirtied
120
+ from OLD and NEW (``()`` = the global ``seq`` only). ``owner_predicate``:
121
+ a SQL predicate over ``{R}`` (the OLD/NEW row) restricting tracking to the
122
+ Claude-owned rows, or ``None``."""
123
+
124
+ compared: "tuple[str, ...]"
125
+ excluded: "tuple[str, ...]"
126
+ insert_rule: str
127
+ scopes: "tuple[str, ...]"
128
+ owner_predicate: "str | None" = None
129
+
130
+
131
+ class ScopeRow(NamedTuple):
132
+ kind: str
133
+ account_key: str
134
+ scope_key: str
135
+ start_epoch: "int | None"
136
+ end_epoch: "int | None"
137
+ revision: int
138
+
139
+
140
+ # ── §4.2 the classification table ─────────────────────────────────────────
141
+
142
+ TRACKED_TABLES: "Mapping[str, TableClassification]" = types.MappingProxyType({
143
+ "weekly_usage_snapshots": TableClassification(
144
+ compared=(
145
+ "id", "captured_at_utc", "week_start_date", "week_end_date",
146
+ "week_start_at", "week_end_at", "weekly_percent", "page_url",
147
+ "source", "payload_json", "account_key",
148
+ "weekly_observation_held", "five_hour_percent",
149
+ "five_hour_resets_at", "five_hour_window_key", "journal_id",
150
+ ),
151
+ excluded=(), insert_rule="below_max", scopes=("bucket", "block"),
152
+ ),
153
+ "weekly_cost_snapshots": TableClassification(
154
+ compared=(
155
+ "id", "captured_at_utc", "week_start_date", "week_end_date",
156
+ "week_start_at", "week_end_at", "range_start_iso",
157
+ "range_end_iso", "cost_usd", "source", "mode", "project",
158
+ "account_key", "journal_id",
159
+ ),
160
+ excluded=(), insert_rule="below_max", scopes=("bucket",),
161
+ ),
162
+ "percent_milestones": TableClassification(
163
+ compared=(
164
+ "id", "captured_at_utc", "week_start_date", "week_end_date",
165
+ "week_start_at", "week_end_at", "percent_threshold",
166
+ "cumulative_cost_usd", "marginal_cost_usd", "usage_snapshot_id",
167
+ "cost_snapshot_id", "reset_event_id", "account_key",
168
+ "five_hour_percent_at_crossing", "alerted_at", "journal_id",
169
+ ),
170
+ excluded=(), insert_rule="tracked", scopes=("cohort", "bucket"),
171
+ ),
172
+ "five_hour_milestones": TableClassification(
173
+ compared=(
174
+ "id", "block_id", "five_hour_window_key", "percent_threshold",
175
+ "captured_at_utc", "usage_snapshot_id", "block_input_tokens",
176
+ "block_output_tokens", "block_cache_create_tokens",
177
+ "block_cache_read_tokens", "block_cost_usd", "marginal_cost_usd",
178
+ "seven_day_pct_at_crossing", "reset_event_id", "account_key",
179
+ "alerted_at", "journal_id",
180
+ ),
181
+ excluded=(), insert_rule="tracked", scopes=("block",),
182
+ ),
183
+ "five_hour_blocks": TableClassification(
184
+ compared=(
185
+ "id", "five_hour_window_key", "five_hour_resets_at",
186
+ "block_start_at", "first_observed_at_utc", "last_observed_at_utc",
187
+ "final_five_hour_percent", "seven_day_pct_at_block_start",
188
+ "seven_day_pct_at_block_end", "crossed_seven_day_reset",
189
+ "total_input_tokens", "total_output_tokens",
190
+ "total_cache_create_tokens", "total_cache_read_tokens",
191
+ "total_cost_usd", "pricing_provenance_json", "is_closed",
192
+ "account_key", "journal_id",
193
+ ),
194
+ excluded=("created_at_utc", "last_updated_at_utc"),
195
+ insert_rule="tracked", scopes=("block",),
196
+ ),
197
+ "five_hour_block_models": TableClassification(
198
+ compared=(
199
+ "id", "block_id", "five_hour_window_key", "model",
200
+ "input_tokens", "output_tokens", "cache_create_tokens",
201
+ "cache_read_tokens", "cost_usd", "entry_count", "account_key",
202
+ ),
203
+ excluded=(), insert_rule="tracked", scopes=(),
204
+ ),
205
+ "five_hour_block_projects": TableClassification(
206
+ compared=(
207
+ "id", "block_id", "five_hour_window_key", "project_path",
208
+ "input_tokens", "output_tokens", "cache_create_tokens",
209
+ "cache_read_tokens", "cost_usd", "entry_count", "account_key",
210
+ ),
211
+ excluded=(), insert_rule="tracked", scopes=(),
212
+ ),
213
+ "five_hour_reset_events": TableClassification(
214
+ compared=(
215
+ "id", "detected_at_utc", "five_hour_window_key", "prior_percent",
216
+ "post_percent", "effective_reset_at_utc", "account_key",
217
+ "journal_id",
218
+ ),
219
+ excluded=(), insert_rule="tracked", scopes=("block",),
220
+ ),
221
+ "week_reset_events": TableClassification(
222
+ compared=(
223
+ "id", "detected_at_utc", "old_week_end_at", "new_week_end_at",
224
+ "effective_reset_at_utc", "observed_pre_credit_pct",
225
+ "account_key", "origin_observation_id", "journal_id",
226
+ ),
227
+ excluded=(), insert_rule="exempt", scopes=("reset_point",),
228
+ ),
229
+ "weekly_credit_floors": TableClassification(
230
+ compared=(
231
+ "id", "week_start_date", "effective_at_utc",
232
+ "observed_pre_credit_pct", "account_key", "journal_id",
233
+ ),
234
+ excluded=("applied_at_utc",), insert_rule="exempt", scopes=(),
235
+ ),
236
+ "claude_weekly_observation_index": TableClassification(
237
+ compared=(
238
+ "observation_id", "account_key", "captured_at_utc",
239
+ "weekly_percent", "weekly_resets_at_utc",
240
+ "weekly_reading_genuine", "weekly_freshness_eligible",
241
+ ),
242
+ excluded=(), insert_rule="exempt", scopes=(),
243
+ ),
244
+ # The Claude ownership predicates of `_CLAUDE_STATS_DIGEST_RELATIONS`
245
+ # (`bin/_lib_dashboard_sources.py`): Codex budget rows are not Claude
246
+ # content, and every project budget row is (that relation has no
247
+ # predicate, so neither does its row here).
248
+ "budget_milestones": TableClassification(
249
+ compared=(
250
+ "id", "vendor", "period_start_at", "period", "threshold",
251
+ "budget_usd", "spent_usd", "consumption_pct", "crossed_at_utc",
252
+ "alerted_at", "account_key", "journal_id",
253
+ ),
254
+ excluded=(), insert_rule="untracked", scopes=(),
255
+ owner_predicate="{R}.vendor <> 'codex'",
256
+ ),
257
+ "projected_milestones": TableClassification(
258
+ compared=(
259
+ "id", "week_start_at", "period", "metric", "threshold",
260
+ "projected_value", "denominator", "crossed_at_utc", "alerted_at",
261
+ "account_key", "journal_id",
262
+ ),
263
+ excluded=(), insert_rule="untracked", scopes=(),
264
+ owner_predicate="{R}.metric <> 'codex_budget_usd'",
265
+ ),
266
+ "project_budget_milestones": TableClassification(
267
+ compared=(
268
+ "id", "week_start_at", "project_key", "threshold", "budget_usd",
269
+ "spent_usd", "consumption_pct", "crossed_at_utc", "alerted_at",
270
+ "account_key", "journal_id",
271
+ ),
272
+ excluded=(), insert_rule="untracked", scopes=(),
273
+ ),
274
+ })
275
+
276
+
277
+ # ── trigger SQL ───────────────────────────────────────────────────────────
278
+
279
+ _SEQ_ADVANCE = (
280
+ "UPDATE stats_content_revision SET seq = seq + 1 WHERE singleton = 1;"
281
+ )
282
+ _SEQ_VALUE = "(SELECT seq FROM stats_content_revision WHERE singleton = 1)"
283
+ # `MIN(COALESCE(start_epoch, x), x)` widening, with the incoming side also
284
+ # coalesced so a NULL epoch (an unparseable timestamp, or a scope kind that
285
+ # has no range) never erases a recorded one: SQLite's multi-argument MIN/MAX
286
+ # return NULL when any argument is NULL.
287
+ _UPSERT_TAIL = (
288
+ " ON CONFLICT (kind, account_key, scope_key) DO UPDATE SET "
289
+ "start_epoch = MIN(COALESCE(start_epoch, excluded.start_epoch), "
290
+ "COALESCE(excluded.start_epoch, start_epoch)), "
291
+ "end_epoch = MAX(COALESCE(end_epoch, excluded.end_epoch), "
292
+ "COALESCE(excluded.end_epoch, end_epoch)), "
293
+ "revision = excluded.revision;"
294
+ )
295
+ _UPSERT_HEAD = (
296
+ "INSERT INTO stats_content_revision_scopes "
297
+ "(kind, account_key, scope_key, start_epoch, end_epoch, revision) "
298
+ )
299
+
300
+
301
+ class _ScopeSelect(NamedTuple):
302
+ """One scope lookup over the row alias ``{R}``: the select list (kind,
303
+ account, key, start, end), an optional FROM and the WHERE. The WHERE is
304
+ never empty: a FROM-carrying SELECT followed by ``ON CONFLICT`` is the
305
+ documented UPSERT parsing ambiguity, so even a FROM-less one ends with
306
+ ``WHERE true``."""
307
+
308
+ columns: str
309
+ source: str
310
+ where: str
311
+
312
+
313
+ _BLOCK_COLUMNS = (
314
+ "'block', b.account_key, CAST(b.five_hour_window_key AS TEXT), "
315
+ "unixepoch(b.block_start_at), unixepoch(b.five_hour_resets_at)"
316
+ )
317
+
318
+
319
+ def _scope_selects(table: str) -> "tuple[_ScopeSelect, ...]":
320
+ """The scope lookups one row of ``table`` dirties, over alias ``{R}``."""
321
+ if table in ("weekly_usage_snapshots",):
322
+ return (
323
+ _ScopeSelect(
324
+ "'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
325
+ "", "true"),
326
+ # Every block whose five-hour milestone references this snapshot
327
+ # (the milestone publishes the snapshot's weekly value).
328
+ _ScopeSelect(
329
+ _BLOCK_COLUMNS,
330
+ "FROM five_hour_milestones AS m, five_hour_blocks AS b",
331
+ "m.usage_snapshot_id = {R}.id AND b.id = m.block_id"),
332
+ )
333
+ if table == "weekly_cost_snapshots":
334
+ return (
335
+ _ScopeSelect(
336
+ "'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
337
+ "", "true"),
338
+ )
339
+ if table == "percent_milestones":
340
+ return (
341
+ _ScopeSelect(
342
+ "'cohort', {R}.account_key, "
343
+ "{R}.week_start_date || '|' || {R}.reset_event_id, NULL, NULL",
344
+ "", "true"),
345
+ _ScopeSelect(
346
+ "'bucket', {R}.account_key, {R}.week_start_date, NULL, NULL",
347
+ "", "true"),
348
+ )
349
+ if table == "five_hour_milestones":
350
+ # The parent block by `block_id`, else by its natural key.
351
+ return (
352
+ _ScopeSelect(
353
+ _BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
354
+ "b.id = {R}.block_id"),
355
+ _ScopeSelect(
356
+ _BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
357
+ "b.account_key = {R}.account_key "
358
+ "AND b.five_hour_window_key = {R}.five_hour_window_key "
359
+ "AND NOT EXISTS (SELECT 1 FROM five_hour_blocks AS p "
360
+ "WHERE p.id = {R}.block_id)"),
361
+ )
362
+ if table == "five_hour_blocks":
363
+ return (
364
+ _ScopeSelect(
365
+ "'block', {R}.account_key, "
366
+ "CAST({R}.five_hour_window_key AS TEXT), "
367
+ "unixepoch({R}.block_start_at), "
368
+ "unixepoch({R}.five_hour_resets_at)",
369
+ "", "true"),
370
+ )
371
+ if table == "five_hour_reset_events":
372
+ return (
373
+ _ScopeSelect(
374
+ _BLOCK_COLUMNS, "FROM five_hour_blocks AS b",
375
+ "b.account_key = {R}.account_key "
376
+ "AND b.five_hour_window_key = {R}.five_hour_window_key"),
377
+ )
378
+ if table == "week_reset_events":
379
+ return tuple(
380
+ _ScopeSelect(
381
+ f"'reset_point', {{R}}.account_key, "
382
+ f"CAST(unixepoch({{R}}.{column}) AS TEXT), "
383
+ f"unixepoch({{R}}.{column}), unixepoch({{R}}.{column})",
384
+ "", f"unixepoch({{R}}.{column}) IS NOT NULL")
385
+ for column in (
386
+ "effective_reset_at_utc", "old_week_end_at",
387
+ "new_week_end_at")
388
+ )
389
+ return ()
390
+
391
+
392
+ def _render(template: str, row: str) -> str:
393
+ return template.replace("{R}", row)
394
+
395
+
396
+ def _upsert(select: _ScopeSelect, row: str) -> str:
397
+ source = f" {_render(select.source, row)}" if select.source else ""
398
+ return (
399
+ f"{_UPSERT_HEAD}SELECT {_render(select.columns, row)}, {_SEQ_VALUE}"
400
+ f"{source} WHERE {_render(select.where, row)}{_UPSERT_TAIL}"
401
+ )
402
+
403
+
404
+ def _body(table: str, rows: "Sequence[str]") -> str:
405
+ statements = [_SEQ_ADVANCE]
406
+ for row in rows:
407
+ statements.extend(_upsert(s, row) for s in _scope_selects(table))
408
+ return " BEGIN " + " ".join(statements) + " END"
409
+
410
+
411
+ def _owner(cls: TableClassification, row: str) -> "str | None":
412
+ if cls.owner_predicate is None:
413
+ return None
414
+ return f"({_render(cls.owner_predicate, row)})"
415
+
416
+
417
+ def _trigger_sql() -> "dict[str, str]":
418
+ """Every kernel trigger, name → its exact `sqlite_schema.sql`."""
419
+ out: "dict[str, str]" = {}
420
+ for table, cls in TRACKED_TABLES.items():
421
+ diff = " OR ".join(f"OLD.{c} IS NOT NEW.{c}" for c in cls.compared)
422
+ update_when = f"({diff})"
423
+ if cls.owner_predicate is not None:
424
+ update_when += f" AND ({_owner(cls, 'OLD')} OR {_owner(cls, 'NEW')})"
425
+ out[f"{TRIGGER_PREFIX}{table}_update"] = (
426
+ f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_update "
427
+ f"AFTER UPDATE ON {table} WHEN {update_when}"
428
+ f"{_body(table, ('OLD', 'NEW'))}"
429
+ )
430
+ delete_when = (
431
+ f" WHEN {_owner(cls, 'OLD')}"
432
+ if cls.owner_predicate is not None else ""
433
+ )
434
+ out[f"{TRIGGER_PREFIX}{table}_delete"] = (
435
+ f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_delete "
436
+ f"AFTER DELETE ON {table}{delete_when}{_body(table, ('OLD',))}"
437
+ )
438
+ if cls.insert_rule in ("tracked", "below_max"):
439
+ insert_when = (
440
+ f" WHEN EXISTS (SELECT 1 FROM {table} WHERE id > NEW.id)"
441
+ if cls.insert_rule == "below_max" else ""
442
+ )
443
+ out[f"{TRIGGER_PREFIX}{table}_insert"] = (
444
+ f"CREATE TRIGGER {TRIGGER_PREFIX}{table}_insert "
445
+ f"AFTER INSERT ON {table}{insert_when}"
446
+ f"{_body(table, ('NEW',))}"
447
+ )
448
+ return out
449
+
450
+
451
+ TRIGGER_SQL: "Mapping[str, str]" = types.MappingProxyType(_trigger_sql())
452
+ #: Every kernel-owned object, name → SQL: what the rebuild contract requires.
453
+ KERNEL_TABLES = (REVISION_TABLE, SCOPE_TABLE)
454
+ KERNEL_INDEXES = (USAGE_SNAPSHOT_INDEX,)
455
+ KERNEL_TRIGGERS = tuple(sorted(TRIGGER_SQL))
456
+ _TABLE_SQL = {REVISION_TABLE: REVISION_TABLE_SQL, SCOPE_TABLE: SCOPE_TABLE_SQL}
457
+
458
+
459
+ def trigger_lookup_queries() -> "tuple[tuple[str, str], ...]":
460
+ """Every trigger lookup as a standalone query over named parameters, for
461
+ `EXPLAIN QUERY PLAN` (§8.4): each scope SELECT with ``{R}.col`` bound as
462
+ ``:col``, and the below-max INSERT predicate."""
463
+ out = []
464
+ for table, cls in TRACKED_TABLES.items():
465
+ for index, select in enumerate(_scope_selects(table)):
466
+ source = f" {select.source}" if select.source else ""
467
+ sql = re.sub(
468
+ r"\{R\}\.(\w+)", r":\1",
469
+ f"SELECT {select.columns}{source} WHERE {select.where}")
470
+ out.append((f"{table}#{index}", sql))
471
+ if cls.insert_rule == "below_max":
472
+ out.append((f"{table}#below_max",
473
+ f"SELECT EXISTS (SELECT 1 FROM {table} WHERE id > :id)"))
474
+ out.append(("seq#advance",
475
+ "UPDATE stats_content_revision SET seq = seq + 1 "
476
+ "WHERE singleton = 1"))
477
+ out.append(("scope#upsert",
478
+ "SELECT revision FROM stats_content_revision_scopes "
479
+ "WHERE kind = :kind AND account_key = :account_key "
480
+ "AND scope_key = :scope_key"))
481
+ return tuple(out)
482
+
483
+
484
+ # ── classification (reads only) ───────────────────────────────────────────
485
+
486
+ _SCHEMA_READ = (
487
+ "SELECT type, name, sql FROM sqlite_schema "
488
+ "WHERE name IN (?, ?, ?) "
489
+ "OR (type = 'trigger' AND substr(name, 1, ?) = ?)"
490
+ )
491
+
492
+
493
+ def _inspect(conn) -> "tuple[str, tuple | None]":
494
+ """The §4.3 state and, when the singleton is valid, its
495
+ ``(format_version, domain_generation, seq)``."""
496
+ objects = conn.execute(
497
+ _SCHEMA_READ,
498
+ (REVISION_TABLE, SCOPE_TABLE, USAGE_SNAPSHOT_INDEX,
499
+ len(TRIGGER_PREFIX), TRIGGER_PREFIX),
500
+ ).fetchall()
501
+ tables = {str(r[1]): r[2] for r in objects if r[0] == "table"}
502
+ if not tables:
503
+ return STATE_ABSENT, None
504
+ if len(tables) != 2 or any(
505
+ tables[name] != sql for name, sql in _TABLE_SQL.items()):
506
+ return STATE_MALFORMED, None
507
+ try:
508
+ rows = conn.execute(
509
+ "SELECT singleton, format_version, domain_generation, seq "
510
+ "FROM stats_content_revision LIMIT 2"
511
+ ).fetchall()
512
+ except sqlite3.Error:
513
+ return STATE_MALFORMED, None
514
+ if len(rows) != 1:
515
+ return STATE_MALFORMED, None
516
+ values = tuple(rows[0])
517
+ if any(type(v) is not int for v in values):
518
+ return STATE_MALFORMED, None
519
+ singleton, format_version, generation, seq = values
520
+ if singleton != 1 or format_version < 1 or generation < 0 or seq < 0:
521
+ return STATE_MALFORMED, None
522
+ singleton_values = (format_version, generation, seq)
523
+ if format_version > KERNEL_FORMAT_VERSION:
524
+ return STATE_UNSUPPORTED, singleton_values
525
+ if format_version < KERNEL_FORMAT_VERSION:
526
+ return STATE_PARTIAL, singleton_values
527
+ present = {
528
+ str(r[1]): r[2] for r in objects if r[0] in ("trigger", "index")
529
+ }
530
+ expected = dict(TRIGGER_SQL)
531
+ expected[USAGE_SNAPSHOT_INDEX] = USAGE_SNAPSHOT_INDEX_SQL
532
+ if present != expected:
533
+ return STATE_PARTIAL, singleton_values
534
+ return STATE_CURRENT, singleton_values
535
+
536
+
537
+ def classify(conn) -> str:
538
+ """The store's §4.3 installation state. Reads only."""
539
+ return _inspect(conn)[0]
540
+
541
+
542
+ # ── installation ──────────────────────────────────────────────────────────
543
+
544
+ def _default_guard(live: bool):
545
+ store = importlib.import_module("_cctally_store")
546
+ return store.stats_open_time_guard(live=live, wait_seconds=0.0)
547
+
548
+
549
+ def _contention_errors() -> "tuple[type, ...]":
550
+ db = importlib.import_module("_cctally_db")
551
+ return (db.StatsDbMaintenanceError,)
552
+
553
+
554
+ def _is_busy(exc: BaseException) -> bool:
555
+ return isinstance(exc, sqlite3.OperationalError) and (
556
+ "locked" in str(exc) or "busy" in str(exc))
557
+
558
+
559
+ def _install(conn, state: str, singleton) -> None:
560
+ """Create or repair every kernel object; the caller owns the transaction.
561
+ Triggers are created last, so seeding the singleton fires nothing."""
562
+ for row in conn.execute(
563
+ "SELECT name FROM sqlite_schema WHERE type = 'trigger' "
564
+ "AND substr(name, 1, ?) = ?",
565
+ (len(TRIGGER_PREFIX), TRIGGER_PREFIX)).fetchall():
566
+ conn.execute(f'DROP TRIGGER IF EXISTS "{row[0]}"')
567
+ conn.execute(f"DROP INDEX IF EXISTS {USAGE_SNAPSHOT_INDEX}")
568
+ if state == STATE_ABSENT:
569
+ conn.execute(REVISION_TABLE_SQL)
570
+ conn.execute(SCOPE_TABLE_SQL)
571
+ conn.execute(
572
+ "INSERT INTO stats_content_revision "
573
+ "(singleton, format_version, domain_generation, seq) "
574
+ "VALUES (1, ?, 0, 0)", (KERNEL_FORMAT_VERSION,))
575
+ else:
576
+ # A repair is a new tracking domain over existing content: rows may
577
+ # have moved while tracking was incomplete, so the identity moves
578
+ # once. `seq` only ever grows.
579
+ _format, generation, seq = singleton
580
+ conn.execute(
581
+ "UPDATE stats_content_revision SET format_version = ?, "
582
+ "domain_generation = ?, seq = ? WHERE singleton = 1",
583
+ (KERNEL_FORMAT_VERSION, generation + 1, seq + 1))
584
+ conn.execute(USAGE_SNAPSHOT_INDEX_SQL)
585
+ for name in KERNEL_TRIGGERS:
586
+ conn.execute(TRIGGER_SQL[name])
587
+
588
+
589
+ def ensure_stats_content_revision(conn, *, live: bool, guard=None) -> str:
590
+ """Install or repair the kernel's objects when needed; return the state
591
+ after the call (§4.3).
592
+
593
+ ``current``, ``unsupported`` and ``malformed`` stores are returned as they
594
+ are, with nothing written and no lock taken. An ``absent`` or ``partial``
595
+ store is reclassified and acted on under the open-time maintenance guard
596
+ (zero wait) in one transaction. Contention — on the guard or on SQLite's
597
+ own write lock — leaves the store unchanged and returns its state; any
598
+ other failure propagates. ``guard`` overrides the default
599
+ ``stats_open_time_guard(live=live, wait_seconds=0.0)`` (a test seam).
600
+ """
601
+ state = classify(conn)
602
+ if state not in (STATE_ABSENT, STATE_PARTIAL):
603
+ return state
604
+ contention = _contention_errors()
605
+ previous_timeout = None
606
+ try:
607
+ with (guard(live) if guard is not None else _default_guard(live)):
608
+ state, singleton = _inspect(conn)
609
+ if state not in (STATE_ABSENT, STATE_PARTIAL):
610
+ return state
611
+ # SQLite's own write lock is contention too: an opener must not
612
+ # wait out another writer's transaction (busy_timeout 15 s) for
613
+ # optional work the next open retries.
614
+ previous_timeout = int(
615
+ conn.execute("PRAGMA busy_timeout").fetchone()[0])
616
+ conn.execute("PRAGMA busy_timeout = 0")
617
+ own = not conn.in_transaction
618
+ conn.execute("BEGIN IMMEDIATE" if own else "SAVEPOINT scr_install")
619
+ try:
620
+ state, singleton = _inspect(conn)
621
+ if state in (STATE_ABSENT, STATE_PARTIAL):
622
+ _install(conn, state, singleton)
623
+ if own:
624
+ conn.commit()
625
+ else:
626
+ conn.execute("RELEASE scr_install")
627
+ except BaseException:
628
+ if own:
629
+ conn.rollback()
630
+ else:
631
+ conn.execute("ROLLBACK TO scr_install")
632
+ conn.execute("RELEASE scr_install")
633
+ raise
634
+ except contention:
635
+ pass
636
+ except sqlite3.OperationalError as exc:
637
+ if not _is_busy(exc):
638
+ raise
639
+ finally:
640
+ if previous_timeout is not None:
641
+ conn.execute(f"PRAGMA busy_timeout = {previous_timeout:d}")
642
+ return classify(conn)
643
+
644
+
645
+ # ── the identity ──────────────────────────────────────────────────────────
646
+
647
+ def _canonical(value) -> str:
648
+ return json.dumps(value, sort_keys=True, separators=(",", ":"),
649
+ ensure_ascii=True)
650
+
651
+
652
+ def _stamp_rows(conn) -> list:
653
+ try:
654
+ rows = conn.execute(
655
+ "SELECT record_path, started_at_utc, stamped_at_utc "
656
+ "FROM stats_publication_stamp").fetchall()
657
+ except sqlite3.OperationalError as exc:
658
+ if "no such table" in str(exc):
659
+ return []
660
+ raise
661
+ # `record_path` names a private file; only its digest takes part.
662
+ return sorted(
663
+ [hashlib.sha256(str(r[0]).encode("utf-8")).hexdigest(),
664
+ None if r[1] is None else str(r[1]),
665
+ None if r[2] is None else str(r[2])]
666
+ for r in rows)
667
+
668
+
669
+ def read_claude_content_revision(stats_conn) -> ContentRevision:
670
+ """Read the identity (§4.4). Joins an open transaction without ending it;
671
+ otherwise owns one short ``BEGIN`` … ``ROLLBACK``. Raises
672
+ `StatsContentRevisionUnreadable` on a ``malformed`` store or more than one
673
+ publication stamp row."""
674
+ own = not stats_conn.in_transaction
675
+ if own:
676
+ stats_conn.execute("BEGIN")
677
+ try:
678
+ state, singleton = _inspect(stats_conn)
679
+ if state == STATE_MALFORMED:
680
+ raise StatsContentRevisionUnreadable(
681
+ "stats content revision objects are malformed")
682
+ stamps = _stamp_rows(stats_conn)
683
+ if len(stamps) > 1:
684
+ raise StatsContentRevisionUnreadable(
685
+ f"stats_publication_stamp holds {len(stamps)} rows")
686
+ if state != STATE_CURRENT:
687
+ return ContentRevision(state, None, None)
688
+ format_version, generation, seq = singleton
689
+ stamp_value = stamps or None
690
+ if seq == 0 and generation == 0 and stamp_value is None:
691
+ token = ""
692
+ else:
693
+ user_version = int(
694
+ stats_conn.execute("PRAGMA user_version").fetchone()[0])
695
+ token = hashlib.sha256(_canonical(
696
+ [format_version, generation, user_version, stamp_value, seq]
697
+ ).encode("utf-8")).hexdigest()[:16]
698
+ cycle_basis = (
699
+ None if generation == 0 and stamp_value is None
700
+ else _canonical([format_version, generation, stamp_value])
701
+ )
702
+ return ContentRevision(state, token, cycle_basis)
703
+ finally:
704
+ if own:
705
+ stats_conn.rollback()
706
+
707
+
708
+ def revision_leg(rev: ContentRevision) -> str:
709
+ """The published leg: the token (``""`` included), else a fresh nonce,
710
+ so an untrusted store never compares equal across reads."""
711
+ if isinstance(rev.token, str):
712
+ return rev.token
713
+ return "?" + secrets.token_hex(8)
714
+
715
+
716
+ def comparable_identity(rev: ContentRevision) -> tuple:
717
+ """What a build bracket compares: stable across reads of an unchanged
718
+ store in every state, different on any state change."""
719
+ if rev.state == STATE_CURRENT:
720
+ return (rev.state, rev.token)
721
+ return (rev.state,)
722
+
723
+
724
+ def read_scope_rows(stats_conn, *, account_key: "str | None") -> "list[ScopeRow]":
725
+ """Every scope row, in primary-key order. Strict to ``account_key`` when
726
+ it is given; merged across every account (``unattributed`` included) when
727
+ it is ``None``. An absent store has none."""
728
+ sql = (
729
+ "SELECT kind, account_key, scope_key, start_epoch, end_epoch, revision "
730
+ "FROM stats_content_revision_scopes"
731
+ )
732
+ params: tuple = ()
733
+ if account_key is not None:
734
+ sql += " WHERE account_key = ?"
735
+ params = (account_key,)
736
+ sql += " ORDER BY kind, account_key, scope_key"
737
+ try:
738
+ rows = stats_conn.execute(sql, params).fetchall()
739
+ except sqlite3.OperationalError as exc:
740
+ if "no such table" in str(exc):
741
+ return []
742
+ raise
743
+ return [ScopeRow(*tuple(r)) for r in rows]
744
+
745
+
746
+ # ── identical child replacement (§4.2) ────────────────────────────────────
747
+ #
748
+ # The open-block projection and a block-close fold or convergence replace a
749
+ # block's model and project rows wholesale. Deleting and re-inserting an
750
+ # identical set would fire the tracked DELETE and INSERT triggers and move
751
+ # `seq` with nothing changed, so the writers first ask whether the stored set
752
+ # already equals the replacement, compared as multisets over every persisted
753
+ # column except `id`. A set that is missing, or differs anywhere, is replaced
754
+ # as before (and so still advances `seq`); inequality is the safe answer to
755
+ # anything this cannot decide.
756
+
757
+ CHILD_TABLES = ("five_hour_block_models", "five_hour_block_projects")
758
+ _CHILD_SELECTS = types.MappingProxyType({
759
+ (table, scoped): (
760
+ f"SELECT * FROM {table} WHERE "
761
+ + ("account_key = ? AND " if scoped else "")
762
+ + "five_hour_window_key = ?"
763
+ )
764
+ for table in CHILD_TABLES
765
+ for scoped in (True, False)
766
+ })
767
+ #: No ORDER BY: the column order only has to agree between the stored and the
768
+ #: wanted tuples, which both take it from this one read, and sorting the
769
+ #: pragma's rows costs a temp B-tree on the live record path.
770
+ _CHILD_COLUMNS = "SELECT name, dflt_value FROM pragma_table_info(?)"
771
+ _UNDECIDABLE = object()
772
+
773
+
774
+ def _default_value(text):
775
+ """The value a plain-literal column DEFAULT stores, ``None`` for no
776
+ default or ``NULL``, and ``_UNDECIDABLE`` for any expression."""
777
+ if text is None:
778
+ return None
779
+ value = text.strip()
780
+ while value.startswith("(") and value.endswith(")"):
781
+ value = value[1:-1].strip()
782
+ if value.upper() == "NULL":
783
+ return None
784
+ if len(value) >= 2 and value[0] == value[-1] == "'":
785
+ return value[1:-1].replace("''", "'")
786
+ for parse in (int, float):
787
+ try:
788
+ return parse(value)
789
+ except ValueError:
790
+ pass
791
+ return _UNDECIDABLE
792
+
793
+
794
+ def child_set_unchanged(conn, table: str, *, five_hour_window_key: int,
795
+ account_key: "str | None",
796
+ replacement: "Sequence[Mapping]") -> bool:
797
+ """True iff ``table``'s rows for the window (and ``account_key`` when it
798
+ is not ``None``, the writer's own DELETE predicate) equal ``replacement``
799
+ as a multiset over every column except ``id``.
800
+
801
+ ``replacement`` holds the column → value mappings the writer would
802
+ INSERT; a column a mapping omits takes the table's literal DEFAULT. A
803
+ mapping naming ``id`` or an unknown column, or an omitted column whose
804
+ default is an expression, is never equal. Values compare as Python does,
805
+ so an INSERT that SQLite's affinity would coerce (``'3'`` into an
806
+ INTEGER column) compares unequal and the set is replaced."""
807
+ if table not in CHILD_TABLES:
808
+ raise ValueError(f"not a five-hour child table: {table}")
809
+ from collections import Counter
810
+ declared = conn.execute(_CHILD_COLUMNS, (table,)).fetchall()
811
+ defaults = {str(name): _default_value(dflt) for name, dflt in declared}
812
+ columns = [name for name in defaults if name != "id"]
813
+ wanted: Counter = Counter()
814
+ for row in replacement:
815
+ if "id" in row or any(name not in defaults for name in row):
816
+ return False
817
+ values = tuple(
818
+ row[name] if name in row else defaults[name] for name in columns)
819
+ if any(value is _UNDECIDABLE for value in values):
820
+ return False
821
+ wanted[values] += 1
822
+ scoped = account_key is not None
823
+ params = ((account_key, int(five_hour_window_key)) if scoped
824
+ else (int(five_hour_window_key),))
825
+ cursor = conn.execute(_CHILD_SELECTS[(table, scoped)], params)
826
+ names = [d[0] for d in cursor.description]
827
+ stored: Counter = Counter(
828
+ tuple(dict(zip(names, tuple(r)))[name] for name in columns)
829
+ for r in cursor.fetchall()
830
+ )
831
+ return stored == wanted