cctally 1.103.0 → 1.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/bin/_cctally_alerts.py +65 -4
- package/bin/_cctally_config.py +62 -2
- package/bin/_cctally_core.py +62 -1
- package/bin/_cctally_dashboard.py +11 -0
- package/bin/_cctally_dashboard_envelope.py +319 -14
- package/bin/_cctally_dashboard_share.py +56 -25
- package/bin/_cctally_doctor.py +63 -0
- package/bin/_cctally_forecast.py +917 -44
- package/bin/_cctally_journal.py +153 -5
- package/bin/_cctally_parser.py +66 -0
- package/bin/_cctally_project.py +535 -10
- package/bin/_cctally_quota.py +13 -0
- package/bin/_cctally_quota_calibration.py +146 -0
- package/bin/_cctally_quota_model.py +1616 -0
- package/bin/_cctally_record.py +114 -6
- package/bin/_cctally_share.py +16 -8
- package/bin/_cctally_statusline.py +34 -0
- package/bin/_cctally_tui.py +214 -45
- package/bin/_lib_dashboard_settings_contract.py +2 -0
- package/bin/_lib_doctor.py +159 -1
- package/bin/_lib_forecast.py +337 -43
- package/bin/_lib_meter_rate_change.py +294 -0
- package/bin/_lib_quota_calibration.py +311 -0
- package/bin/_lib_quota_copy.py +131 -0
- package/bin/_lib_quota_model.py +2333 -0
- package/bin/_lib_rederive.py +10 -0
- package/bin/_lib_render.py +6 -0
- package/bin/_lib_share_templates.py +37 -5
- package/bin/_lib_statusline.py +226 -2
- package/bin/_lib_view_models.py +30 -12
- package/bin/cctally +32 -0
- package/dashboard/static/assets/index-D19TO7Mg.js +97 -0
- package/dashboard/static/assets/index-klO46NcU.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +7 -1
- package/dashboard/static/assets/index-Di2hljvB.css +0 -1
- package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The non-mutating read of the persisted quota calibration (#661 S2).
|
|
2
|
+
|
|
3
|
+
The only I/O half of the §1.1 adapters: open the sidecar, hand its bytes to
|
|
4
|
+
the pure validator in `bin/_lib_quota_calibration.py`, and report a typed
|
|
5
|
+
cause when anything about it is unusable.
|
|
6
|
+
|
|
7
|
+
WHY THIS EXISTS RATHER THAN A CALL TO `load_calibrations`. That loader
|
|
8
|
+
QUARANTINES a malformed or version-ahead file by renaming it aside. `doctor`
|
|
9
|
+
is documented read-only, the status line runs per prompt, and the dashboard
|
|
10
|
+
serves a read request — a rename from any of them makes a documented reader a
|
|
11
|
+
writer, and the rename is invisible to the user who triggered it. This module
|
|
12
|
+
therefore never imports or calls `load_calibrations`, `_quarantine`, or
|
|
13
|
+
`save_calibrations`, and takes no lock: `save_calibrations` publishes through
|
|
14
|
+
`os.replace` in the same directory, so a lock-free reader always observes a
|
|
15
|
+
complete old or new inode, and a flock here would let a writer stall a prompt.
|
|
16
|
+
|
|
17
|
+
Spec: docs/superpowers/specs/2026-08-28-661-s2-quota-insight-surfaces.md §1.1
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
import _cctally_quota_model
|
|
25
|
+
import _lib_quota_model as qm
|
|
26
|
+
from _lib_quota_calibration import ( # re-exported for callers and tests
|
|
27
|
+
AppliedQuota, ApplyRejection, RegimeRejection, ValidatedRegime,
|
|
28
|
+
apply_regime, validate_regime,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AppliedQuota", "ApplyRejection", "CalibrationRead", "RegimeRejection",
|
|
33
|
+
"ValidatedRegime", "apply_regime", "read_calibration_file",
|
|
34
|
+
"validate_regime",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclasses.dataclass(frozen=True)
|
|
39
|
+
class CalibrationRead:
|
|
40
|
+
"""Exactly one of `regime` and `rejection` is set.
|
|
41
|
+
|
|
42
|
+
`regime_status` carries the stored `status` of the regime the prediction
|
|
43
|
+
gate refused, and is `None` in every other outcome. S1 records WHY it held
|
|
44
|
+
a successor fit below the gate, and a surface that states
|
|
45
|
+
`insufficient-history` while the store says `unstable-fit` names a cause
|
|
46
|
+
the reader cannot reconcile with `cctally quota`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
regime: "ValidatedRegime | None"
|
|
50
|
+
rejection: "RegimeRejection | None"
|
|
51
|
+
regime_status: "str | None" = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_text(path) -> str:
|
|
55
|
+
"""Open and read in one step, with NO prior existence check.
|
|
56
|
+
|
|
57
|
+
A concurrent quarantine rename can remove the primary name between a
|
|
58
|
+
check and an open, so a `path.exists()` guard would turn a race into a
|
|
59
|
+
traceback. The absent case arrives here as `OSError` like every other
|
|
60
|
+
failure and is separated by its errno at the call site.
|
|
61
|
+
"""
|
|
62
|
+
with open(str(path), "r", encoding="utf-8") as handle:
|
|
63
|
+
return handle.read()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _state_key(account_key) -> str:
|
|
67
|
+
"""The stored bucket name for an account. `None` means the merged view."""
|
|
68
|
+
return (_cctally_quota_model.MERGED_STATE_KEY if account_key is None
|
|
69
|
+
else str(account_key))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_calibration_file(*, account_key, path=None,
|
|
73
|
+
expected_fingerprint=None,
|
|
74
|
+
expected_revision=None,
|
|
75
|
+
require_prediction_ready=True) -> CalibrationRead:
|
|
76
|
+
"""The open regime for one account, validated, or a typed cause.
|
|
77
|
+
|
|
78
|
+
`expected_fingerprint` and `expected_revision` default to THIS binary's
|
|
79
|
+
constants, so a caller that names neither still refuses a regime fitted
|
|
80
|
+
under other coefficients rather than applying it silently.
|
|
81
|
+
|
|
82
|
+
`require_prediction_ready` defaults to True, so every projection consumer
|
|
83
|
+
is refused a regime S1 marked `detection-only`. A detection consumer —
|
|
84
|
+
section 6's rate-change alert — passes False and reads the same record.
|
|
85
|
+
"""
|
|
86
|
+
if path is None:
|
|
87
|
+
path = _cctally_quota_model.calibration_path()
|
|
88
|
+
if expected_fingerprint is None:
|
|
89
|
+
expected_fingerprint = qm.QUOTA_MODEL_CONSTANTS_FINGERPRINT
|
|
90
|
+
if expected_revision is None:
|
|
91
|
+
expected_revision = qm.QUOTA_MODEL_ALGORITHM_REVISION
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
raw = _read_text(path)
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
return CalibrationRead(None, RegimeRejection.ABSENT)
|
|
97
|
+
except (IsADirectoryError, NotADirectoryError):
|
|
98
|
+
return CalibrationRead(None, RegimeRejection.ABSENT)
|
|
99
|
+
except OSError:
|
|
100
|
+
return CalibrationRead(None, RegimeRejection.UNREADABLE)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
state = json.loads(raw)
|
|
104
|
+
except ValueError:
|
|
105
|
+
return CalibrationRead(None, RegimeRejection.MALFORMED)
|
|
106
|
+
if not isinstance(state, dict):
|
|
107
|
+
return CalibrationRead(None, RegimeRejection.MALFORMED)
|
|
108
|
+
|
|
109
|
+
version = state.get("schemaVersion")
|
|
110
|
+
if isinstance(version, bool) or not isinstance(version, int) \
|
|
111
|
+
or version > _cctally_quota_model.CALIBRATION_STATE_SCHEMA_VERSION:
|
|
112
|
+
return CalibrationRead(None, RegimeRejection.SCHEMA_VERSION)
|
|
113
|
+
|
|
114
|
+
accounts = state.get("accounts")
|
|
115
|
+
if not isinstance(accounts, dict):
|
|
116
|
+
return CalibrationRead(None, RegimeRejection.MALFORMED)
|
|
117
|
+
bucket = accounts.get(_state_key(account_key))
|
|
118
|
+
regimes = bucket.get("regimes") if isinstance(bucket, dict) else None
|
|
119
|
+
if not isinstance(regimes, list):
|
|
120
|
+
return CalibrationRead(None, RegimeRejection.ABSENT)
|
|
121
|
+
|
|
122
|
+
# The LAST open regime, matching `_cctally_quota_model._open_regime`. A
|
|
123
|
+
# store carrying several open records is malformed history rather than a
|
|
124
|
+
# choice this reader makes, and the newest one is what a writer would
|
|
125
|
+
# have updated in place.
|
|
126
|
+
open_regime = None
|
|
127
|
+
for candidate in reversed(regimes):
|
|
128
|
+
if isinstance(candidate, dict) and candidate.get("effectiveUntil") \
|
|
129
|
+
is None:
|
|
130
|
+
open_regime = candidate
|
|
131
|
+
break
|
|
132
|
+
if open_regime is None:
|
|
133
|
+
return CalibrationRead(None, RegimeRejection.ABSENT)
|
|
134
|
+
|
|
135
|
+
outcome = validate_regime(
|
|
136
|
+
open_regime, account_key=account_key,
|
|
137
|
+
expected_fingerprint=expected_fingerprint,
|
|
138
|
+
expected_revision=expected_revision,
|
|
139
|
+
require_prediction_ready=require_prediction_ready)
|
|
140
|
+
if isinstance(outcome, RegimeRejection):
|
|
141
|
+
status = None
|
|
142
|
+
if outcome is RegimeRejection.DETECTION_ONLY:
|
|
143
|
+
stored = open_regime.get("status")
|
|
144
|
+
status = str(stored) if stored is not None else None
|
|
145
|
+
return CalibrationRead(None, outcome, status)
|
|
146
|
+
return CalibrationRead(outcome, None)
|