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.
Files changed (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/bin/_cctally_alerts.py +65 -4
  3. package/bin/_cctally_config.py +62 -2
  4. package/bin/_cctally_core.py +62 -1
  5. package/bin/_cctally_dashboard.py +11 -0
  6. package/bin/_cctally_dashboard_envelope.py +319 -14
  7. package/bin/_cctally_dashboard_share.py +56 -25
  8. package/bin/_cctally_doctor.py +63 -0
  9. package/bin/_cctally_forecast.py +917 -44
  10. package/bin/_cctally_journal.py +153 -5
  11. package/bin/_cctally_parser.py +66 -0
  12. package/bin/_cctally_project.py +535 -10
  13. package/bin/_cctally_quota.py +13 -0
  14. package/bin/_cctally_quota_calibration.py +146 -0
  15. package/bin/_cctally_quota_model.py +1616 -0
  16. package/bin/_cctally_record.py +114 -6
  17. package/bin/_cctally_share.py +16 -8
  18. package/bin/_cctally_statusline.py +34 -0
  19. package/bin/_cctally_tui.py +214 -45
  20. package/bin/_lib_dashboard_settings_contract.py +2 -0
  21. package/bin/_lib_doctor.py +159 -1
  22. package/bin/_lib_forecast.py +337 -43
  23. package/bin/_lib_meter_rate_change.py +294 -0
  24. package/bin/_lib_quota_calibration.py +311 -0
  25. package/bin/_lib_quota_copy.py +131 -0
  26. package/bin/_lib_quota_model.py +2333 -0
  27. package/bin/_lib_rederive.py +10 -0
  28. package/bin/_lib_render.py +6 -0
  29. package/bin/_lib_share_templates.py +37 -5
  30. package/bin/_lib_statusline.py +226 -2
  31. package/bin/_lib_view_models.py +30 -12
  32. package/bin/cctally +32 -0
  33. package/dashboard/static/assets/index-D19TO7Mg.js +97 -0
  34. package/dashboard/static/assets/index-klO46NcU.css +1 -0
  35. package/dashboard/static/dashboard.html +2 -2
  36. package/package.json +7 -1
  37. package/dashboard/static/assets/index-Di2hljvB.css +0 -1
  38. package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
@@ -0,0 +1,131 @@
1
+ """#661 S2 Task D1 — the ONE cause-code copy table (spec section 8).
2
+
3
+ S1 publishes fifteen withheld-figure codes in `EVIDENCE_CODES`. Section 8
4
+ renders them through one table in two registers, and this module is that
5
+ table.
6
+
7
+ **Short register.** A fixed-width slot — the status line, a panel chip, a
8
+ table cell — where a sentence does not fit. It is DERIVED from the code by
9
+ `derive_short_from_code`, a pure function of the string, and `short_form` is
10
+ that derivation. Deriving rather than curating is the point: a client that
11
+ receives no presentation fields must still render something, and a
12
+ derivation it can reimplement in four lines cannot fall out of step with a
13
+ table it never sees.
14
+
15
+ **Long register.** A full sentence, for a modal, a footer or a `--help`-style
16
+ report. This is what a client cannot derive and what the wire's presentation
17
+ fields exist to carry.
18
+
19
+ **The wire keeps the machine code.** `code` is unchanged and stays the stable
20
+ machine-readable value; the presentation fields sit ALONGSIDE it. `--json`
21
+ keeps emitting the code, because full sentences do not replace machine codes
22
+ there.
23
+
24
+ The degraded path is therefore: no presentation fields on the wire, so the
25
+ client shows the derived short token and no sentence. Never blank, which
26
+ matters because a dashboard tab can outlive a server restart through
27
+ `execvp` and will meet an older server's envelope.
28
+
29
+ Pure: no I/O, no clock, no store. The vocabulary it covers is the closed
30
+ 15-member `EVIDENCE_CODES` union, and `tests/test_quota_copy_table.py`
31
+ asserts both registers exist for every member — a code added to the kernel
32
+ without copy fails there rather than rendering as a bare token in a modal.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ #: Full sentences, keyed by the machine code. Curated, because a sentence is
37
+ #: exactly what a client cannot derive. A code missing from this table falls
38
+ #: back to its short form, so the long register degrades to the short one
39
+ #: rather than to nothing — but the test above means that never ships.
40
+ _LONG_FORM: dict = {
41
+ # --- WithholdingCause: why one daily observation carries no value ---
42
+ "no-local-history":
43
+ "no local entries cover this day, so there is nothing to model from",
44
+ "sparse-local-history":
45
+ "this day holds some local usage, but below the fence the fit needs",
46
+ "transition-day":
47
+ "this day sits on a metering-rate boundary, so its usage was priced "
48
+ "under two different rates",
49
+ "right-censored":
50
+ "the meter reached its ceiling, so the real consumption is only "
51
+ "known to be at least the reading",
52
+ "token-split-unknown":
53
+ "the token split for part of this window is unknown, so the "
54
+ "weighted units cannot be computed",
55
+ "unsupported-composition":
56
+ "this window's model and token mix sits outside the calibration's "
57
+ "support",
58
+ # --- CalibrationStatus: the state of one account's calibration ---
59
+ "insufficient-history":
60
+ "not enough local history has accumulated to fit a rate yet",
61
+ "fragmented-history":
62
+ "the local history has too many gaps to fit a rate across",
63
+ "unstable-fit":
64
+ "the fit did not settle on one rate, so no single value describes "
65
+ "this account",
66
+ "local-history-incomplete":
67
+ "the local entry history does not cover the whole window being "
68
+ "modelled",
69
+ "unsupported-model-mix":
70
+ "the models in use sit outside the mix the calibration was fitted "
71
+ "over",
72
+ "unvalidated-coefficient-era":
73
+ "this window predates the token weights the model is validated for",
74
+ "stale":
75
+ "the stored calibration was fitted under different constants, so it "
76
+ "no longer describes this build",
77
+ "future":
78
+ "the stored calibration came from a newer cctally than this one",
79
+ "unavailable":
80
+ "no usable calibration could be read for this account",
81
+ }
82
+
83
+
84
+ def derive_short_from_code(code) -> str:
85
+ """The short token, derived from the code alone.
86
+
87
+ The whole derivation: the hyphens become spaces. It is written out as a
88
+ named function rather than inlined because a client that receives no
89
+ presentation fields reimplements exactly this, and a rule stated in one
90
+ place is a rule two implementations can be checked against.
91
+
92
+ Never returns an empty string for a non-empty code, which is the property
93
+ the degraded path depends on.
94
+ """
95
+ text = str(code or "").strip()
96
+ if not text:
97
+ return ""
98
+ return text.replace("-", " ")
99
+
100
+
101
+ def short_form(code) -> str:
102
+ """The fixed-width register. Identical to `derive_short_from_code`.
103
+
104
+ Two names for one function, deliberately: `short_form` is what a renderer
105
+ asks for, and `derive_short_from_code` is what a client without the wire
106
+ fields reimplements. Keeping them the same function is what makes the
107
+ degraded path a degradation in the SENTENCE and not in the token.
108
+ """
109
+ return derive_short_from_code(code)
110
+
111
+
112
+ def long_form(code) -> str:
113
+ """The sentence register, or the short token when no sentence exists."""
114
+ text = str(code or "").strip()
115
+ if not text:
116
+ return ""
117
+ return _LONG_FORM.get(text) or derive_short_from_code(text)
118
+
119
+
120
+ def presentation(code) -> dict:
121
+ """The three fields a wire payload carries for one code.
122
+
123
+ `code` is the stable machine value and is repeated here so a consumer
124
+ holding only this dict still has it. The two rendering fields are
125
+ additive: an older server sends neither and the client derives the short
126
+ one.
127
+ """
128
+ text = str(code or "").strip()
129
+ if not text:
130
+ return {}
131
+ return {"code": text, "short": short_form(text), "long": long_form(text)}