cctally 1.114.0 → 1.115.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 (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4394 -279
  4. package/bin/_cctally_completion.py +1287 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +178 -2
  7. package/bin/_cctally_dashboard.py +2 -2
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +16 -3
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +316 -5
  12. package/bin/_cctally_doctor.py +436 -27
  13. package/bin/_cctally_five_hour.py +82 -31
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +3884 -112
  16. package/bin/_cctally_journal_repair.py +101 -12
  17. package/bin/_cctally_parser.py +42 -0
  18. package/bin/_cctally_project.py +8 -2
  19. package/bin/_cctally_quarantine.py +652 -0
  20. package/bin/_cctally_quota.py +26 -4
  21. package/bin/_cctally_quota_model.py +360 -83
  22. package/bin/_cctally_record.py +677 -149
  23. package/bin/_cctally_rederive.py +297 -32
  24. package/bin/_cctally_refresh.py +84 -13
  25. package/bin/_cctally_source_analytics.py +23 -2
  26. package/bin/_cctally_statusline.py +6137 -517
  27. package/bin/_cctally_store.py +287 -48
  28. package/bin/_cctally_transcript.py +8 -7
  29. package/bin/_cctally_tui.py +57 -53
  30. package/bin/_lib_account_binding.py +152 -0
  31. package/bin/_lib_account_protocol.py +386 -0
  32. package/bin/_lib_aggregators.py +4 -0
  33. package/bin/_lib_cache_coverage.py +13 -1
  34. package/bin/_lib_cache_writer_lock.py +19 -3
  35. package/bin/_lib_clone_copy.py +142 -0
  36. package/bin/_lib_codex_conversation_query.py +133 -19
  37. package/bin/_lib_codex_conversation_watch.py +59 -14
  38. package/bin/_lib_codex_drain_admission.py +214 -0
  39. package/bin/_lib_codex_metadata.py +18 -1
  40. package/bin/_lib_codex_publication_barrier.py +277 -0
  41. package/bin/_lib_codex_rollout_container.py +502 -0
  42. package/bin/_lib_conversation_dispatch.py +3 -0
  43. package/bin/_lib_conversation_query.py +11 -3
  44. package/bin/_lib_detector_access_registry.py +3817 -0
  45. package/bin/_lib_doctor.py +452 -1
  46. package/bin/_lib_five_hour.py +126 -0
  47. package/bin/_lib_ingest_frontier.py +414 -20
  48. package/bin/_lib_journal.py +141 -10
  49. package/bin/_lib_jsonl.py +5 -4
  50. package/bin/_lib_legacy_spool.py +126 -0
  51. package/bin/_lib_legacy_writer_evidence.py +136 -0
  52. package/bin/_lib_merge_gate.py +29 -302
  53. package/bin/_lib_meter_rate_change.py +10 -0
  54. package/bin/_lib_outcome_quarantine.py +690 -0
  55. package/bin/_lib_pricing.py +7 -2
  56. package/bin/_lib_quarantine.py +154 -0
  57. package/bin/_lib_quota_calibration.py +53 -8
  58. package/bin/_lib_quota_copy.py +2 -1
  59. package/bin/_lib_quota_model.py +288 -46
  60. package/bin/_lib_rederive.py +94 -5
  61. package/bin/_lib_segment_summary.py +35 -2
  62. package/bin/_lib_statusline_candidates.py +292 -23
  63. package/bin/cctally +23 -1
  64. package/package.json +14 -1
@@ -17,6 +17,7 @@ import _cctally_journal as _journal
17
17
  import _lib_accounts
18
18
  import _lib_journal
19
19
  import _lib_journal_router
20
+ import _cctally_store
20
21
  from _lib_json_envelope import stamp_schema_version
21
22
 
22
23
 
@@ -68,11 +69,20 @@ def _read_prefix(high_water):
68
69
  )
69
70
  records = []
70
71
  evidence = []
72
+ evidence_owners: list = []
71
73
  audit_ends: dict = {}
74
+ audit_candidates: list = []
72
75
  malformed = 0
73
76
  prior_high_water = None
74
77
  cutover_captured = _journal._CUTOVER_UNSEEN
75
78
  hasher = _lib_journal_router.PrefixHashAccumulator()
79
+ # #825/#832 (spec 5.3): admission is resolved within this one pass and
80
+ # applied after it, before selection or normalization. An excluded record
81
+ # — an old process's repair acknowledgement or correction batch included —
82
+ # keeps its positional slot and is never selection or audit input (F7).
83
+ pass_boundary = _journal._PassBoundary()
84
+ coordinates: list = []
85
+ cutover_candidates: list = []
76
86
  for segment, offset, raw in _journal._iter_range_with_segments(
77
87
  None,
78
88
  high_water,
@@ -86,6 +96,7 @@ def _read_prefix(high_water):
86
96
  prior_high_water = (segment, offset + len(raw) + 1)
87
97
  continue
88
98
  record_end = (segment, offset + len(raw) + 1)
99
+ pass_boundary.observe(record)
89
100
  if record.get("t") == "op":
90
101
  _journal._capture_protocol_prefix_evidence(
91
102
  record,
@@ -93,20 +104,22 @@ def _read_prefix(high_water):
93
104
  evidence,
94
105
  hasher=hasher,
95
106
  )
107
+ if len(evidence) > len(evidence_owners):
108
+ evidence_owners.append(len(records))
96
109
  payload = record.get("payload")
97
110
  if (
98
111
  isinstance(payload, dict)
99
112
  and payload.get("kind")
100
- == _lib_journal._PROTOCOL_RESOLUTION_KIND
113
+ in _lib_journal.PROTOCOL_RESOLUTION_KINDS
101
114
  ):
102
- audit_ends[record.get("id")] = record_end
103
- # First cutover op wins, exactly as `find_accounts_cutover_op` scans.
104
- if (
105
- cutover_captured is _journal._CUTOVER_UNSEEN
106
- and record.get("id") == _journal.CUTOVER_OP_ID
107
- ):
108
- cutover_captured = _journal._cutover_value_of(record)
115
+ audit_candidates.append(
116
+ (len(records), record.get("id"), record_end))
117
+ # First ADMITTED cutover op wins, exactly as `find_accounts_cutover_op`
118
+ # scans; admission is known only after the pass.
119
+ if record.get("id") == _journal.CUTOVER_OP_ID:
120
+ cutover_candidates.append((record, (segment, offset)))
109
121
  records.append(_lib_journal_router.selector_slot(record))
122
+ coordinates.append((segment, offset))
110
123
  prior_high_water = record_end
111
124
  prefix_hash = hasher.digest_at(high_water)
112
125
  # The accumulator buffers the segment it is reading — 410 MB on the
@@ -119,6 +132,19 @@ def _read_prefix(high_water):
119
132
  raise _lib_journal.JournalProtocolError(
120
133
  f"journal prefix contains {malformed} malformed line(s)"
121
134
  )
135
+ boundary = pass_boundary.resolve(high_water)
136
+ _journal._apply_pass_admission(records, coordinates, boundary)
137
+ # #825/#832 fix round C (CD2-005): an excluded acknowledgement is absent
138
+ # for every purpose — its evidence and its audit end go with it.
139
+ evidence = _journal._admitted_prefix_evidence(
140
+ evidence, evidence_owners, records)
141
+ for index, audit_id, record_end in audit_candidates:
142
+ if records[index] is not None:
143
+ audit_ends[audit_id] = record_end
144
+ for candidate, (segment, offset) in cutover_candidates:
145
+ if _journal._is_admitted(candidate, segment, offset, boundary):
146
+ cutover_captured = _journal._cutover_value_of(candidate)
147
+ break
122
148
  # No suffix fallback, deliberately. `_read_only_high_water` pins the
123
149
  # canonically-last segment at its full size, so this prefix IS the whole
124
150
  # journal and an op the prefix does not contain is not in the journal at
@@ -255,6 +281,13 @@ def _repair_locks(timeout: float = 5.0):
255
281
  finally:
256
282
  _cctally_core.note_stats_maintenance_released()
257
283
  release_cache_writer_flocks(held)
284
+ # #825/#832 spec 4.3 revision 2: the repair's live index publication (or
285
+ # its no-op) posts a wake once its locks are released.
286
+ try:
287
+ import _cctally_statusline
288
+ _cctally_statusline.post_wake("holder:journal-repair")
289
+ except Exception:
290
+ pass
258
291
 
259
292
 
260
293
  def _iso_now() -> str:
@@ -322,6 +355,25 @@ def _post_audit_failure(requested, audit_ids, exc):
322
355
  return payload, 3
323
356
 
324
357
 
358
+ def _post_audit_obligation_lost(requested, audit_ids, exc):
359
+ """Fix round K12 (K11R-3): the rebuilt index IS published (and the
360
+ holder wake is posted once the repair locks are released); only its
361
+ residual-derivation obligation was lost. The status stays `failed`,
362
+ with the additive `indexPublished`, and the command exits 3."""
363
+ payload, returncode = _post_audit_failure(requested, audit_ids, exc)
364
+ payload["indexPublished"] = True
365
+ return payload, returncode
366
+
367
+
368
+ def _obligation_lost_message(error) -> str:
369
+ return (
370
+ "cctally: the corrected stats.db was published, but the record that "
371
+ "the inputs written during the rebuild still need deriving could not "
372
+ f"be saved ({error}). Those inputs stay in the journal and the next "
373
+ "ingest derives them; check that the data directory is writable."
374
+ )
375
+
376
+
325
377
  def _stats_has_acknowledgements(fingerprints) -> bool:
326
378
  if not _cctally_core.DB_PATH.exists():
327
379
  return False
@@ -330,9 +382,12 @@ def _stats_has_acknowledgements(fingerprints) -> bool:
330
382
  f"file:{_cctally_core.DB_PATH}?mode=ro", uri=True
331
383
  )
332
384
  try:
333
- rows = conn.execute(
334
- "SELECT violation_json FROM journal_protocol_violations"
335
- )
385
+ # #825/#832 spec 6: completion evidence only from a validated
386
+ # current index; anything else fails closed.
387
+ with _cctally_store.current_stats_snapshot(conn):
388
+ rows = conn.execute(
389
+ "SELECT violation_json FROM journal_protocol_violations"
390
+ ).fetchall()
336
391
  stored = {
337
392
  item.get("fingerprint")
338
393
  for item in (json.loads(str(row[0])) for row in rows)
@@ -341,7 +396,8 @@ def _stats_has_acknowledgements(fingerprints) -> bool:
341
396
  return set(fingerprints) <= stored
342
397
  finally:
343
398
  conn.close()
344
- except (OSError, sqlite3.Error, ValueError, TypeError):
399
+ except (OSError, sqlite3.Error, ValueError, TypeError,
400
+ _cctally_store.StatsIndexNotCurrent):
345
401
  return False
346
402
 
347
403
 
@@ -368,6 +424,8 @@ def _recovery_high_water(audit_ids, audit_ends):
368
424
 
369
425
 
370
426
  def _apply(requested, initial_preview):
427
+ import _cctally_completion
428
+
371
429
  _call_pause_hook("before-lock")
372
430
  with _repair_locks():
373
431
  preview, snapshot = _preview_payload(requested)
@@ -429,10 +487,17 @@ def _apply(requested, initial_preview):
429
487
  ),
430
488
  high_water=audit_high_water,
431
489
  update_quota_cache=False,
490
+ complete_derivation=True,
432
491
  before_swap=lambda: _call_crash_hook(
433
492
  "before-rebuild-swap"
434
493
  ),
435
494
  )
495
+ except _cctally_completion.ResidualObligationNotDurable as exc:
496
+ return _post_audit_obligation_lost(
497
+ requested,
498
+ {audit["id"]},
499
+ exc,
500
+ )
436
501
  except Exception as exc:
437
502
  return _post_audit_failure(
438
503
  requested,
@@ -477,10 +542,17 @@ def _apply(requested, initial_preview):
477
542
  ),
478
543
  high_water=recovery_high_water,
479
544
  update_quota_cache=False,
545
+ complete_derivation=True,
480
546
  before_swap=lambda: _call_crash_hook(
481
547
  "before-rebuild-swap"
482
548
  ),
483
549
  )
550
+ except _cctally_completion.ResidualObligationNotDurable as exc:
551
+ return _post_audit_obligation_lost(
552
+ requested,
553
+ audit_ids,
554
+ exc,
555
+ )
484
556
  except Exception as exc:
485
557
  return _post_audit_failure(
486
558
  requested,
@@ -498,6 +570,18 @@ def _apply(requested, initial_preview):
498
570
 
499
571
  def cmd_db_journal_repair(args) -> int:
500
572
  """Preview structural violations without mutating the journal or indexes."""
573
+ if (bool(getattr(args, "yes", False))
574
+ and list(getattr(args, "violation", ()) or ())
575
+ and _journal.account_bound_cutover_enforced()):
576
+ # #825/#832 spec 5.5: establish the account-bound cutover before the
577
+ # apply's prefix-bound planning (the preview below is that plan), so
578
+ # the plan never races the boundary's own append.
579
+ try:
580
+ _journal.ensure_account_bound_cutover()
581
+ except _journal.AccountBoundCutoverDeferred:
582
+ pass
583
+ except OSError:
584
+ pass
501
585
  try:
502
586
  requested = list(getattr(args, "violation", ()) or ())
503
587
  payload, _snapshot = _preview_payload(requested)
@@ -585,6 +669,11 @@ def cmd_db_journal_repair(args) -> int:
585
669
  if returncode != 0:
586
670
  if bool(getattr(args, "json", False)):
587
671
  print(json.dumps(payload, separators=(",", ":")))
672
+ elif payload.get("indexPublished"):
673
+ print(
674
+ _obligation_lost_message(payload["errors"][0]),
675
+ file=sys.stderr,
676
+ )
588
677
  else:
589
678
  for error in payload["errors"]:
590
679
  print(
@@ -3787,6 +3787,28 @@ def _build_stats_corruption_heal_parser(subparsers, name, *, help_text, xref=Non
3787
3787
  )
3788
3788
  worker.set_defaults(func=c.cmd_stats_corruption_heal_internal)
3789
3789
 
3790
+ def _build_statusline_continue_parser(subparsers, name, *, help_text, xref=None):
3791
+ """Build the #825/#832 detached statusline continuation worker parser."""
3792
+ c = _cctally()
3793
+ worker = subparsers.add_parser(
3794
+ name,
3795
+ help=help_text,
3796
+ formatter_class=CLIHelpFormatter,
3797
+ description=textwrap.dedent(
3798
+ """\
3799
+ Internal subcommand: continue deferred statusline work
3800
+ under the one reducer reservation it inherits, or (with
3801
+ --wait) observe one blocking lock's release and post one
3802
+ wake. Spawned by the event-driven continuation; never
3803
+ polls. Always returns 0.
3804
+ """
3805
+ ),
3806
+ )
3807
+ worker.add_argument("--reservation-fd", type=int, required=True,
3808
+ help=argparse.SUPPRESS)
3809
+ worker.add_argument("--wait", default=None, help=argparse.SUPPRESS)
3810
+ worker.set_defaults(func=c.cmd_statusline_continue_internal)
3811
+
3790
3812
  def _build_artifact_retention_parser(subparsers, name, *, help_text, xref=None):
3791
3813
  """Build the #496 S6 detached retained-artifact reclamation worker parser."""
3792
3814
  c = _cctally()
@@ -3827,6 +3849,24 @@ def _build_codex_replay_drain_parser(subparsers, name, *, help_text, xref=None):
3827
3849
  )
3828
3850
  rd.set_defaults(func=c.cmd_codex_replay_drain_internal)
3829
3851
 
3852
+ def _build_codex_prune_drain_parser(subparsers, name, *, help_text, xref=None):
3853
+ """Build the `_codex-prune-drain` parser (#968 spec 5.7)."""
3854
+ c = _cctally()
3855
+ pd = subparsers.add_parser(
3856
+ name,
3857
+ help=help_text,
3858
+ formatter_class=CLIHelpFormatter,
3859
+ description=textwrap.dedent(
3860
+ """\
3861
+ Internal subcommand: detached worker that drains recorded
3862
+ Codex prune debt one source identity per transaction, off
3863
+ the hook. Always returns 0; failures are written to
3864
+ hook-tick.log.
3865
+ """
3866
+ ),
3867
+ )
3868
+ pd.set_defaults(func=c.cmd_codex_prune_drain_internal)
3869
+
3830
3870
  def _build_repair_symlinks_parser(subparsers, name, *, help_text, xref=None):
3831
3871
  """Build the `repair-symlinks` parser (registered via _REGISTRATION; #279 S6 W3).
3832
3872
 
@@ -3913,8 +3953,10 @@ _REGISTRATION = (
3913
3953
  _Reg('_codex-quota-verify', _build_codex_quota_verify_parser, argparse.SUPPRESS, None, None),
3914
3954
  _Reg('_stats-epoch-rebuild', _build_stats_epoch_rebuild_parser, argparse.SUPPRESS, None, None),
3915
3955
  _Reg('_stats-corruption-heal', _build_stats_corruption_heal_parser, argparse.SUPPRESS, None, None),
3956
+ _Reg('_statusline-continue', _build_statusline_continue_parser, argparse.SUPPRESS, None, None),
3916
3957
  _Reg('_artifact-retention', _build_artifact_retention_parser, argparse.SUPPRESS, None, None),
3917
3958
  _Reg('_codex-replay-drain', _build_codex_replay_drain_parser, argparse.SUPPRESS, None, None),
3959
+ _Reg('_codex-prune-drain', _build_codex_prune_drain_parser, argparse.SUPPRESS, None, None),
3918
3960
  _Reg('repair-symlinks', _build_repair_symlinks_parser, argparse.SUPPRESS, None, None),
3919
3961
  )
3920
3962
 
@@ -58,9 +58,11 @@ def _cctally():
58
58
  #: Why modelled quota is not published for a week or a whole run. A closed
59
59
  #: set. `account-not-resolved` is the run-level one (§5.3); the rest are
60
60
  #: per-week and are the reasons that week fell back to the cost share.
61
+ #: `unvalidated-coefficient-era` is `apply_regime`'s rejection of a week
62
+ #: outside the stored regime's coefficient era (#978 spec 4.6).
61
63
  ATTRIBUTION_CAUSES = ("account-not-resolved", "calibration-absent",
62
64
  "regime-boundary", "unsupported-composition",
63
- "no-local-history")
65
+ "no-local-history", "unvalidated-coefficient-era")
64
66
 
65
67
  #: The three bases a row or a run can carry.
66
68
  ATTRIBUTION_BASES = ("modelled", "cost-share", "withheld")
@@ -219,7 +221,8 @@ def resolve_week_attribution(regime, records, *, week_start, week_end):
219
221
  and week_end > regime.effective_until:
220
222
  return WeekAttribution("cost-share", "regime-boundary", None)
221
223
  qcg = _cctally()._load_sibling("_cctally_quota_calibration")
222
- applied = qcg.apply_regime(regime, list(records))
224
+ applied = qcg.apply_regime(regime, list(records),
225
+ interval=(week_start, week_end))
223
226
  if isinstance(applied, qcg.ApplyRejection):
224
227
  return WeekAttribution("cost-share", applied.value, None)
225
228
  return WeekAttribution("modelled", None, regime.units_per_point)
@@ -282,6 +285,9 @@ _ATTRIBUTION_CAUSE_COPY: dict = {
282
285
  "unsupported-composition":
283
286
  "the week's model mix sits outside the calibration's support",
284
287
  "no-local-history": "no local entries to model",
288
+ "unvalidated-coefficient-era":
289
+ "the week is not inside the coefficient era the calibration's token "
290
+ "weights are validated for",
285
291
  RESIDUAL_MISALIGNED:
286
292
  "the account, the window or the population does not align",
287
293
  RESIDUAL_OBSERVED_ABSENT: