cctally 1.92.2 → 1.93.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 +43 -0
- package/bin/_cctally_cache.py +354 -0
- package/bin/_cctally_core.py +180 -3
- package/bin/_cctally_dashboard.py +71 -1
- package/bin/_cctally_dashboard_envelope.py +28 -2
- package/bin/_cctally_dashboard_share.py +75 -19
- package/bin/_cctally_dashboard_sources.py +12 -0
- package/bin/_cctally_db.py +89 -1
- package/bin/_cctally_doctor.py +31 -0
- package/bin/_cctally_forecast.py +4 -2
- package/bin/_cctally_journal.py +3482 -258
- package/bin/_cctally_journal_repair.py +123 -32
- package/bin/_cctally_milestone_history.py +4 -1
- package/bin/_cctally_project.py +8 -6
- package/bin/_cctally_quota.py +420 -20
- package/bin/_cctally_rederive.py +57 -23
- package/bin/_cctally_reporting.py +8 -6
- package/bin/_cctally_share.py +74 -37
- package/bin/_cctally_source_analytics.py +6 -8
- package/bin/_cctally_store.py +13 -2
- package/bin/_cctally_tui.py +53 -0
- package/bin/_lib_cache_coverage.py +547 -0
- package/bin/_lib_doctor.py +54 -2
- package/bin/_lib_journal.py +235 -95
- package/bin/_lib_journal_router.py +21 -0
- package/bin/_lib_segment_summary.py +374 -0
- package/bin/_lib_selector_state.py +959 -0
- package/bin/_lib_share.py +1073 -165
- package/bin/_lib_share_templates.py +35 -11
- package/bin/_lib_stats_wal.py +327 -0
- package/bin/_lib_view_models.py +2 -1
- package/dashboard/static/assets/index-DwWJOYxd.css +1 -0
- package/dashboard/static/assets/{index-Dat-mza6.js → index-HlIK7k8Q.js} +47 -47
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-DnWdv8um.css +0 -1
package/bin/_cctally_db.py
CHANGED
|
@@ -993,6 +993,7 @@ def write_corruption_forensics(
|
|
|
993
993
|
bundle: dict = {
|
|
994
994
|
"schemaVersion": 1,
|
|
995
995
|
"capturedAtUtc": _forensics_iso(dt.datetime.now(dt.timezone.utc)),
|
|
996
|
+
"sqliteRuntimeVersion": sqlite3.sqlite_version,
|
|
996
997
|
"db": db_label,
|
|
997
998
|
"path": str(db_path),
|
|
998
999
|
"family": {},
|
|
@@ -1012,12 +1013,37 @@ def write_corruption_forensics(
|
|
|
1012
1013
|
bundle["walEvidence"] = _capture_corruption_wal_evidence(
|
|
1013
1014
|
db_path, ts, db_label=db_label, trigger_exception=trigger_exception,
|
|
1014
1015
|
)
|
|
1016
|
+
try:
|
|
1017
|
+
import _lib_stats_wal
|
|
1018
|
+
|
|
1019
|
+
bundle["walIndexEvidence"] = _lib_stats_wal.inspect_wal_index_family(
|
|
1020
|
+
db_path
|
|
1021
|
+
)
|
|
1022
|
+
except Exception as exc: # noqa: BLE001 — enrichment never breaks a heal
|
|
1023
|
+
bundle["walIndexEvidence"] = {
|
|
1024
|
+
"schemaVersion": 1,
|
|
1025
|
+
"verdict": "unavailable",
|
|
1026
|
+
"captureStable": False,
|
|
1027
|
+
"reason": _bounded_forensics_text(
|
|
1028
|
+
exc, _FORENSICS_EXCEPTION_MESSAGE_MAX,
|
|
1029
|
+
),
|
|
1030
|
+
"wal": None,
|
|
1031
|
+
"shm": None,
|
|
1032
|
+
"frameMapping": {
|
|
1033
|
+
"comparedCount": 0,
|
|
1034
|
+
"mismatchCount": 0,
|
|
1035
|
+
"mismatchSample": [],
|
|
1036
|
+
"truncated": False,
|
|
1037
|
+
},
|
|
1038
|
+
}
|
|
1015
1039
|
for suffix in ("", "-wal", "-shm"):
|
|
1016
1040
|
p = pathlib.Path(str(db_path) + suffix)
|
|
1017
1041
|
try:
|
|
1018
1042
|
st = p.stat()
|
|
1019
1043
|
bundle["family"][p.name] = {
|
|
1020
|
-
"bytes": st.st_size,
|
|
1044
|
+
"bytes": st.st_size,
|
|
1045
|
+
"mtimeUtc": _forensics_iso(st.st_mtime),
|
|
1046
|
+
"inode": int(st.st_ino),
|
|
1021
1047
|
}
|
|
1022
1048
|
except OSError:
|
|
1023
1049
|
bundle["family"][p.name] = None
|
|
@@ -1314,6 +1340,30 @@ def quarantine_db_family(
|
|
|
1314
1340
|
_REBUILD_CONFLICT_SAMPLE = 10
|
|
1315
1341
|
|
|
1316
1342
|
|
|
1343
|
+
def _cache_recovery_payload(coverage) -> dict:
|
|
1344
|
+
"""`db rebuild --json`'s `cacheRecovery` object (#496 S5b §4.7).
|
|
1345
|
+
|
|
1346
|
+
`complete` is NORMALIZED here, and that is the point of the helper. The
|
|
1347
|
+
coverage record starts life as `{"status": "skipped", "reason": None,
|
|
1348
|
+
"replayedObservations": 0}` with no `complete` key at all, and a rebuild run
|
|
1349
|
+
with `update_quota_cache=False` never adds one — so a raw `.get("complete")`
|
|
1350
|
+
emitted `null`, while `_write_quota_projection_state` reads that same
|
|
1351
|
+
absence as COMPLETE. The JSON and the durable flag disagreed about exactly
|
|
1352
|
+
one state, and `if not payload["cacheRecovery"]["complete"]` got the wrong
|
|
1353
|
+
answer for it. The two now agree by construction: an absent key is an absent
|
|
1354
|
+
duty, which is complete, and `phase` says `notRun` so a consumer can still
|
|
1355
|
+
tell "the leg did not run" from "the leg ran and had nothing to do".
|
|
1356
|
+
"""
|
|
1357
|
+
coverage = coverage or {}
|
|
1358
|
+
ran = "complete" in coverage
|
|
1359
|
+
return {
|
|
1360
|
+
"phase": coverage.get("status") if ran else "notRun",
|
|
1361
|
+
"complete": bool(coverage.get("complete")) if ran else True,
|
|
1362
|
+
"coveredHighWater": coverage.get("coveredHighWater"),
|
|
1363
|
+
"remainder": coverage.get("remainder"),
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
|
|
1317
1367
|
def cmd_db_rebuild(args: argparse.Namespace) -> int:
|
|
1318
1368
|
"""``db rebuild --db stats`` — explicit journal replay into a fresh index
|
|
1319
1369
|
(spec §9). Captures forensics first, builds and validates a fresh index while
|
|
@@ -1427,6 +1477,34 @@ def cmd_db_rebuild(args: argparse.Namespace) -> int:
|
|
|
1427
1477
|
violation.to_dict()
|
|
1428
1478
|
for violation in result.acknowledged_protocol_violations
|
|
1429
1479
|
],
|
|
1480
|
+
# #496 S5b — the durable selector prefix of the index this rebuild
|
|
1481
|
+
# replaced, when it was behind that index's own applied cursor.
|
|
1482
|
+
# `null` in the healthy case. Reported here and in the rebuild
|
|
1483
|
+
# record only: the live path's own recovery is silent by §6.3.
|
|
1484
|
+
"selectorDesynchronized": result.selector_desynchronized,
|
|
1485
|
+
# #496 S5b F11 — what the quota cache leg did. `covered` is the
|
|
1486
|
+
# intact path: the coverage certificate proved every cache-relevant
|
|
1487
|
+
# journal record in the pinned prefix was already materialized, so
|
|
1488
|
+
# the leg took no cache writer flock and replayed nothing.
|
|
1489
|
+
"quotaCacheCoverage": result.quota_cache_coverage or None,
|
|
1490
|
+
# #496 S5b §4.7 — publication success and cache-recovery
|
|
1491
|
+
# completeness are DISTINCT, and a consumer must not be able to read
|
|
1492
|
+
# one as the other. Reaching this point means the index was built,
|
|
1493
|
+
# validated and published; whether the cache the quota projection
|
|
1494
|
+
# was materialized from had reached its pinned target is a separate
|
|
1495
|
+
# question with a separate answer.
|
|
1496
|
+
"publication": {
|
|
1497
|
+
"ok": True,
|
|
1498
|
+
"statsQuotaProjectionIncomplete": bool(
|
|
1499
|
+
result.stats_quota_projection_incomplete),
|
|
1500
|
+
},
|
|
1501
|
+
"cacheRecovery": _cache_recovery_payload(
|
|
1502
|
+
result.quota_cache_coverage),
|
|
1503
|
+
# #496 S5b F12 — which journal segments the read pass skipped and,
|
|
1504
|
+
# per segment, why it refused the ones it read. Every refusal is a
|
|
1505
|
+
# silent full read by §6.3, so this and the rebuild record are the
|
|
1506
|
+
# only places that say which state it was.
|
|
1507
|
+
"segmentElision": (result.traversal or {}).get("elision"),
|
|
1430
1508
|
}
|
|
1431
1509
|
print(json.dumps(stamp_schema_version(payload, version=1)))
|
|
1432
1510
|
else:
|
|
@@ -6464,6 +6542,16 @@ def _024_codex_fused_ingest_rebuild(conn: sqlite3.Connection) -> None:
|
|
|
6464
6542
|
conn.execute(
|
|
6465
6543
|
"DELETE FROM cache_meta WHERE key='codex_quota_projection_certificate'"
|
|
6466
6544
|
)
|
|
6545
|
+
# #496 S5b §4.3, the identical hazard one layer over: this deletes
|
|
6546
|
+
# every Codex `quota_window_snapshots` row while the journal still
|
|
6547
|
+
# retains the observations behind them, and it does NOT bump
|
|
6548
|
+
# `codex_physical_mutation_seq`, so a stored coverage certificate
|
|
6549
|
+
# would stay stale-VALID and the next rebuild's fast path would skip
|
|
6550
|
+
# a replay over a cache holding zero Codex quota rows. Reachable
|
|
6551
|
+
# through `db skip 024` followed by `db unskip 024`. Same
|
|
6552
|
+
# transaction as the deletes.
|
|
6553
|
+
import _cctally_cache
|
|
6554
|
+
_cctally_cache._invalidate_codex_journal_coverage_certificate(conn)
|
|
6467
6555
|
conn.commit()
|
|
6468
6556
|
except Exception:
|
|
6469
6557
|
conn.rollback()
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -1730,6 +1730,35 @@ def _doctor_gather_state_impl(
|
|
|
1730
1730
|
journal_cursor_lag_bytes = lag
|
|
1731
1731
|
except Exception:
|
|
1732
1732
|
pass
|
|
1733
|
+
# #496 S5b: the durable incomplete-quota-projection flag carried inside the
|
|
1734
|
+
# published stats generation. Read-only, and independent of journal presence
|
|
1735
|
+
# because the flag describes the INDEX rather than the journal. None means
|
|
1736
|
+
# "no epoch-1009 index to ask" (absent file, missing table, unreadable DB),
|
|
1737
|
+
# which the pure kernel reports as not applicable rather than as a fault.
|
|
1738
|
+
#
|
|
1739
|
+
# This is a THIRD read-only stats open in this function, and folding it into
|
|
1740
|
+
# the `jc` open above was considered and rejected: that open sits inside
|
|
1741
|
+
# `if journal_present:`, so carrying this SELECT there would make the flag
|
|
1742
|
+
# unreadable on an install whose journal directory is absent — exactly the
|
|
1743
|
+
# independence the paragraph above states. One extra guarded open on the
|
|
1744
|
+
# doctor path is the cheaper of the two.
|
|
1745
|
+
stats_quota_projection_incomplete: "bool | None" = None
|
|
1746
|
+
try:
|
|
1747
|
+
if _cctally_core.DB_PATH.exists():
|
|
1748
|
+
qp = _stats_ro_guarded() # #386 opener protocol
|
|
1749
|
+
try:
|
|
1750
|
+
row = qp.execute(
|
|
1751
|
+
"SELECT incomplete FROM stats_quota_projection_state "
|
|
1752
|
+
"WHERE id = 1").fetchone()
|
|
1753
|
+
if row is not None:
|
|
1754
|
+
stats_quota_projection_incomplete = bool(int(row[0] or 0))
|
|
1755
|
+
except sqlite3.OperationalError:
|
|
1756
|
+
pass # pre-1009 index has no stats_quota_projection_state
|
|
1757
|
+
finally:
|
|
1758
|
+
qp.close()
|
|
1759
|
+
except Exception:
|
|
1760
|
+
stats_quota_projection_incomplete = None
|
|
1761
|
+
|
|
1733
1762
|
# Auto-heal incident history — independent of journal presence (a corruption
|
|
1734
1763
|
# incident can predate cutover). None only if BOTH dirs were unreadable.
|
|
1735
1764
|
journal_heal_incidents = None
|
|
@@ -1856,6 +1885,8 @@ def _doctor_gather_state_impl(
|
|
|
1856
1885
|
journal_protocol_violations=journal_protocol_violations,
|
|
1857
1886
|
journal_protocol_acknowledged=journal_protocol_acknowledged,
|
|
1858
1887
|
journal_protocol_error=journal_protocol_error,
|
|
1888
|
+
# #496 S5b: the published generation's incomplete-quota-projection flag.
|
|
1889
|
+
stats_quota_projection_incomplete=stats_quota_projection_incomplete,
|
|
1859
1890
|
# #315: read-only cache free-page evidence for the reclaim hint.
|
|
1860
1891
|
cache_db_page_count=cache_db_page_count,
|
|
1861
1892
|
cache_db_freelist_count=cache_db_freelist_count,
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -2913,7 +2913,8 @@ def _print_project_section_or_note(rows, has_projects, window_resolved, args):
|
|
|
2913
2913
|
|
|
2914
2914
|
def _append_project_share_rows(snap, rows, has_projects):
|
|
2915
2915
|
"""Append per-project ProjectCell rows to a budget ShareSnapshot so the
|
|
2916
|
-
share-output anonymization chokepoint (
|
|
2916
|
+
share-output anonymization chokepoint (the preparation pass
|
|
2917
|
+
``_lib_share.render()`` runs over the raw snapshot) rewrites the
|
|
2917
2918
|
basenames under default output and reveals them under ``--reveal-projects``
|
|
2918
2919
|
(spec §7.5). No-op when projects are empty → existing share goldens stay
|
|
2919
2920
|
byte-identical. Project names go through ``ProjectCell`` (the single
|
|
@@ -3029,7 +3030,8 @@ def _build_budget_snapshot(
|
|
|
3029
3030
|
|
|
3030
3031
|
This builds the GLOBAL budget rows only; when per-project budgets are
|
|
3031
3032
|
configured, `_append_project_share_rows` appends ProjectCell rows so
|
|
3032
|
-
`--reveal-projects` reveals (or `
|
|
3033
|
+
`--reveal-projects` reveals (or, by default, `render()`'s preparation
|
|
3034
|
+
pass anonymizes) the per-project
|
|
3033
3035
|
basenames via the share chokepoint. No parallel renderer; the gate calls
|
|
3034
3036
|
`_share_render_and_emit(snap, args)`.
|
|
3035
3037
|
|