cctally 1.50.0 → 1.51.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 +8 -0
- package/bin/_cctally_core.py +76 -0
- package/bin/_cctally_dashboard.py +22 -1
- package/bin/_cctally_parser.py +45 -0
- package/bin/_cctally_project.py +46 -3
- package/bin/_cctally_record.py +519 -25
- package/bin/_cctally_statusline.py +16 -14
- package/bin/cctally +20 -0
- package/dashboard/static/assets/index-CUHxiTTx.js +68 -0
- package/dashboard/static/assets/index-DAf-3Eal.css +1 -0
- package/dashboard/static/dashboard.html +3 -2
- package/dashboard/static/favicon.svg +1 -0
- package/package.json +1 -1
- package/dashboard/static/assets/index-C96dGQ7N.js +0 -68
- package/dashboard/static/assets/index-ChyJiUWl.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -170,6 +170,7 @@ from _cctally_core import (
|
|
|
170
170
|
compute_week_bounds,
|
|
171
171
|
parse_date_str,
|
|
172
172
|
_canonicalize_optional_iso,
|
|
173
|
+
_reset_aware_floor,
|
|
173
174
|
make_week_ref,
|
|
174
175
|
_get_alerts_config,
|
|
175
176
|
_AlertsConfigError,
|
|
@@ -2174,6 +2175,78 @@ def _read_reset_zero_marker():
|
|
|
2174
2175
|
return (week_start_date, cur_end_canon, baseline_pct, first_zero_iso)
|
|
2175
2176
|
|
|
2176
2177
|
|
|
2178
|
+
@dataclass(frozen=True)
|
|
2179
|
+
class CreditPlan:
|
|
2180
|
+
week_start_date: str
|
|
2181
|
+
week_start_at: str
|
|
2182
|
+
week_end_at: str
|
|
2183
|
+
cur_end_canon: str
|
|
2184
|
+
from_pct: float
|
|
2185
|
+
from_source: str # "hwm" | "explicit" | "prior_credit"
|
|
2186
|
+
to_pct: float
|
|
2187
|
+
effective_iso: str # weekly_credit_floors.effective_at_utc (floored to hour)
|
|
2188
|
+
captured_iso: str # synthetic snapshot captured_at_utc (un-floored), 'Z'
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
def _parse_credit_at(value, now):
|
|
2192
|
+
"""Parse --at as an aware UTC datetime; default to `now`. Naive => UTC
|
|
2193
|
+
(mirrors bin/_cctally_five_hour.py:88), NOT parse_iso_datetime (host-local)."""
|
|
2194
|
+
if value is None:
|
|
2195
|
+
return now
|
|
2196
|
+
try:
|
|
2197
|
+
parsed = dt.datetime.fromisoformat(value)
|
|
2198
|
+
except ValueError as e:
|
|
2199
|
+
raise ValueError(f"--at: {e}") from e
|
|
2200
|
+
if parsed.tzinfo is None:
|
|
2201
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
2202
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
def _build_credit_plan(*, week_start_date, week_start_at, week_end_at,
|
|
2206
|
+
from_pct, from_source, to_pct, at_dt, now,
|
|
2207
|
+
effective_override=None):
|
|
2208
|
+
"""Validate inputs and build a CreditPlan. Pure (no DB/file I/O).
|
|
2209
|
+
Raises ValueError(msg) on any violation — caller maps to exit 2.
|
|
2210
|
+
|
|
2211
|
+
`effective_override` (an ISO string) is the completion-path reuse of an
|
|
2212
|
+
EXISTING `weekly_credit_floors.effective_at_utc` (spec §4a): when present,
|
|
2213
|
+
the plan's effective is that value rather than `floor_to_hour(at)`, so a
|
|
2214
|
+
rerun of a half-applied credit at a later wall-clock keeps the original
|
|
2215
|
+
floor moment and never leaks a stale pre-credit replay into the floored
|
|
2216
|
+
MAX. The synthetic snapshot's captured timestamp stays the un-floored
|
|
2217
|
+
`at` either way."""
|
|
2218
|
+
to_pct = _normalize_percent(to_pct)
|
|
2219
|
+
from_pct = _normalize_percent(from_pct)
|
|
2220
|
+
if not (0.0 <= to_pct <= 100.0) or not (0.0 <= from_pct <= 100.0):
|
|
2221
|
+
raise ValueError("--to/--from must be in [0, 100]")
|
|
2222
|
+
if to_pct >= from_pct:
|
|
2223
|
+
raise ValueError(f"not a credit: to >= from ({to_pct} >= {from_pct})")
|
|
2224
|
+
ws = parse_iso_datetime(week_start_at, "week_start_at")
|
|
2225
|
+
we = parse_iso_datetime(week_end_at, "week_end_at")
|
|
2226
|
+
if not (ws <= at_dt < we):
|
|
2227
|
+
raise ValueError(f"--at {at_dt.isoformat()} is outside the week window")
|
|
2228
|
+
if at_dt > now:
|
|
2229
|
+
raise ValueError("--at is in the future")
|
|
2230
|
+
if effective_override is not None:
|
|
2231
|
+
effective_iso = parse_iso_datetime(
|
|
2232
|
+
effective_override, "effective_override"
|
|
2233
|
+
).astimezone(dt.timezone.utc).isoformat(timespec="seconds")
|
|
2234
|
+
else:
|
|
2235
|
+
effective_iso = _floor_to_hour(at_dt).isoformat(timespec="seconds")
|
|
2236
|
+
cur_end_canon = _canonicalize_optional_iso(week_end_at, "record-credit.week_end")
|
|
2237
|
+
return CreditPlan(
|
|
2238
|
+
week_start_date=week_start_date,
|
|
2239
|
+
week_start_at=week_start_at,
|
|
2240
|
+
week_end_at=week_end_at,
|
|
2241
|
+
cur_end_canon=cur_end_canon,
|
|
2242
|
+
from_pct=from_pct,
|
|
2243
|
+
from_source=from_source,
|
|
2244
|
+
to_pct=to_pct,
|
|
2245
|
+
effective_iso=effective_iso,
|
|
2246
|
+
captured_iso=at_dt.isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
|
|
2177
2250
|
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2178
2251
|
*, observed_pre_credit_pct, effective_dt):
|
|
2179
2252
|
"""Emit/refresh the in-place weekly-credit artifacts (issue #19 + #128).
|
|
@@ -2238,6 +2311,422 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2238
2311
|
eprint(f"[record-usage] post-credit cleanup failed: {exc}")
|
|
2239
2312
|
|
|
2240
2313
|
|
|
2314
|
+
def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at):
|
|
2315
|
+
"""The floored MAX(weekly_percent) the statusline _hwm_clamp computes:
|
|
2316
|
+
MAX over snapshots captured at/after the latest in-week clamp floor. The
|
|
2317
|
+
floor is the latest effective across BOTH `week_reset_events` and
|
|
2318
|
+
`weekly_credit_floors` (`_reset_aware_floor`) so a manual partial credit
|
|
2319
|
+
(record-credit M2, #209) lowers the resolved HWM without re-anchoring the
|
|
2320
|
+
week — used both as the `--from` default and as the assertion source of
|
|
2321
|
+
truth in the record-credit tests."""
|
|
2322
|
+
floor_iso = _reset_aware_floor(conn, week_start_date, week_start_at, week_end_at)
|
|
2323
|
+
if floor_iso is not None:
|
|
2324
|
+
row = conn.execute(
|
|
2325
|
+
"SELECT MAX(weekly_percent) FROM weekly_usage_snapshots "
|
|
2326
|
+
" WHERE week_start_date = ? AND unixepoch(captured_at_utc) >= unixepoch(?)",
|
|
2327
|
+
(week_start_date, floor_iso),
|
|
2328
|
+
).fetchone()
|
|
2329
|
+
else:
|
|
2330
|
+
row = conn.execute(
|
|
2331
|
+
"SELECT MAX(weekly_percent) FROM weekly_usage_snapshots "
|
|
2332
|
+
" WHERE week_start_date = ?",
|
|
2333
|
+
(week_start_date,),
|
|
2334
|
+
).fetchone()
|
|
2335
|
+
return None if not row or row[0] is None else float(row[0])
|
|
2336
|
+
|
|
2337
|
+
|
|
2338
|
+
def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None)):
|
|
2339
|
+
"""Insert the post-credit snapshot at plan.to_pct, tagged source='record-credit'."""
|
|
2340
|
+
fhp, fhr, fhk = five_hour
|
|
2341
|
+
# Normalize effective to a +00:00 UTC spelling in the payload (matches the
|
|
2342
|
+
# stored weekly_credit_floors.effective_at_utc on a non-UTC host).
|
|
2343
|
+
effective_utc = parse_iso_datetime(
|
|
2344
|
+
plan.effective_iso, "snapshot.effective"
|
|
2345
|
+
).astimezone(dt.timezone.utc).isoformat(timespec="seconds")
|
|
2346
|
+
payload = json.dumps(
|
|
2347
|
+
{"kind": "record-credit", "from": plan.from_pct, "to": plan.to_pct,
|
|
2348
|
+
"effective": effective_utc},
|
|
2349
|
+
separators=(",", ":"),
|
|
2350
|
+
)
|
|
2351
|
+
conn.execute(
|
|
2352
|
+
"INSERT INTO weekly_usage_snapshots "
|
|
2353
|
+
"(captured_at_utc, week_start_date, week_end_date, week_start_at, "
|
|
2354
|
+
" week_end_at, weekly_percent, page_url, source, payload_json, "
|
|
2355
|
+
" five_hour_percent, five_hour_resets_at, five_hour_window_key) "
|
|
2356
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
2357
|
+
(plan.captured_iso, plan.week_start_date,
|
|
2358
|
+
parse_iso_datetime(plan.week_end_at, "we").date().isoformat(),
|
|
2359
|
+
plan.week_start_at, plan.week_end_at, plan.to_pct, None,
|
|
2360
|
+
"record-credit", payload, fhp, fhr, fhk),
|
|
2361
|
+
)
|
|
2362
|
+
conn.commit()
|
|
2363
|
+
return conn.total_changes
|
|
2364
|
+
|
|
2365
|
+
|
|
2366
|
+
def _resolve_prior_5h(conn, at_dt):
|
|
2367
|
+
"""Return the most-recent snapshot's (five_hour_percent, five_hour_resets_at,
|
|
2368
|
+
five_hour_window_key) iff that 5h window is still active (resets_at > at_dt),
|
|
2369
|
+
else (None, None, None) — so the synthetic row doesn't blank the live 5h
|
|
2370
|
+
display, and never inflates the 5h HWM (copies an already-<=MAX value)."""
|
|
2371
|
+
row = conn.execute(
|
|
2372
|
+
"SELECT five_hour_percent, five_hour_resets_at, five_hour_window_key "
|
|
2373
|
+
"FROM weekly_usage_snapshots "
|
|
2374
|
+
"WHERE five_hour_resets_at IS NOT NULL AND five_hour_window_key IS NOT NULL "
|
|
2375
|
+
"ORDER BY unixepoch(captured_at_utc) DESC, id DESC LIMIT 1").fetchone()
|
|
2376
|
+
if row is None:
|
|
2377
|
+
return (None, None, None)
|
|
2378
|
+
try:
|
|
2379
|
+
if parse_iso_datetime(row[1], "prior.5h_resets") > at_dt:
|
|
2380
|
+
return (row[0], row[1], int(row[2]))
|
|
2381
|
+
except ValueError:
|
|
2382
|
+
pass
|
|
2383
|
+
return (None, None, None)
|
|
2384
|
+
|
|
2385
|
+
|
|
2386
|
+
def _apply_credit(conn, plan, *, five_hour=(None, None, None)):
|
|
2387
|
+
"""Apply the M2 same-window partial-credit artifacts (record-credit, #209,
|
|
2388
|
+
spec §4). Unlike the >=25pp auto-credit path (`_fire_in_place_credit`), this
|
|
2389
|
+
writes NO `week_reset_events` row — the window-resolution code never sees a
|
|
2390
|
+
credit, so the week is NOT re-anchored. It only lowers the clamp floor.
|
|
2391
|
+
|
|
2392
|
+
Side-effect ordering mirrors `_fire_in_place_credit`'s discipline: the
|
|
2393
|
+
INSERT OR IGNORE of the floor row is dedup-gated by UNIQUE(week_start_date,
|
|
2394
|
+
effective_at_utc), but the hwm force-write, stale-replay DELETE, and
|
|
2395
|
+
synthetic-snapshot INSERT run UNCONDITIONALLY so a rerun finishes a crash-
|
|
2396
|
+
half-applied credit (memory: project_dedup_must_not_gate_side_effects). All
|
|
2397
|
+
are individually idempotent (file overwrite; DELETE on a stable predicate;
|
|
2398
|
+
the synthetic snapshot is re-INSERTed only after `_force_clear_credit` or on
|
|
2399
|
+
the completion path where none exists yet).
|
|
2400
|
+
|
|
2401
|
+
`plan.effective_iso` is `floor_to_hour(at)`. parse_iso_datetime returns a
|
|
2402
|
+
host-local-offset aware datetime; convert to UTC so `effective_at_utc`
|
|
2403
|
+
persists with a +00:00 spelling, not a host offset, in the `*_utc` column.
|
|
2404
|
+
On the completion / --force re-apply path the CALLER passes a `plan` whose
|
|
2405
|
+
`effective_iso` is the EXISTING floor row's `effective_at_utc` (NOT a fresh
|
|
2406
|
+
floor_to_hour(now)) — spec §4a completion-effective reuse."""
|
|
2407
|
+
c = _cctally()
|
|
2408
|
+
effective_dt = parse_iso_datetime(plan.effective_iso, "effective").astimezone(dt.timezone.utc)
|
|
2409
|
+
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
2410
|
+
pre_credit = float(plan.from_pct)
|
|
2411
|
+
|
|
2412
|
+
# 4a. INSERT the credit floor (no week_reset_events row — the whole point).
|
|
2413
|
+
conn.execute(
|
|
2414
|
+
"INSERT OR IGNORE INTO weekly_credit_floors "
|
|
2415
|
+
"(week_start_date, effective_at_utc, observed_pre_credit_pct, applied_at_utc) "
|
|
2416
|
+
"VALUES (?, ?, ?, ?)",
|
|
2417
|
+
(plan.week_start_date, effective_iso, pre_credit, now_utc_iso()),
|
|
2418
|
+
)
|
|
2419
|
+
conn.commit()
|
|
2420
|
+
|
|
2421
|
+
# 4b. Force-write hwm-7d so the external statusline render reflects the
|
|
2422
|
+
# post-credit value (the normal write-site monotonic guard would refuse to
|
|
2423
|
+
# decrease the file).
|
|
2424
|
+
try:
|
|
2425
|
+
(_cctally_core.APP_DIR / "hwm-7d").write_text(
|
|
2426
|
+
f"{plan.week_start_date} {plan.to_pct}\n"
|
|
2427
|
+
)
|
|
2428
|
+
except OSError:
|
|
2429
|
+
pass
|
|
2430
|
+
|
|
2431
|
+
# 4c. Stale-replay DELETE: drop pre-credit-valued replays that land at/after
|
|
2432
|
+
# the floor (the gotcha_statusline_replay_race_after_credit defense; same
|
|
2433
|
+
# 1.0pp band as the auto path). unixepoch() on both sides for offset safety.
|
|
2434
|
+
try:
|
|
2435
|
+
conn.execute(
|
|
2436
|
+
"DELETE FROM weekly_usage_snapshots "
|
|
2437
|
+
"WHERE week_start_date = ? "
|
|
2438
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2439
|
+
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2440
|
+
(plan.week_start_date, effective_iso, pre_credit),
|
|
2441
|
+
)
|
|
2442
|
+
conn.commit()
|
|
2443
|
+
except sqlite3.DatabaseError as exc:
|
|
2444
|
+
eprint(f"[record-credit] post-credit cleanup failed: {exc}")
|
|
2445
|
+
|
|
2446
|
+
# 4d. INSERT the synthetic post-credit snapshot at plan.to_pct.
|
|
2447
|
+
c._insert_credit_snapshot(conn, plan, five_hour=five_hour)
|
|
2448
|
+
|
|
2449
|
+
# 4e. Clear a stale same-week reset-zero marker so the next record-usage
|
|
2450
|
+
# tick can't confirm a phantom reset-to-zero off it.
|
|
2451
|
+
_clear_reset_zero_marker()
|
|
2452
|
+
|
|
2453
|
+
|
|
2454
|
+
def _force_clear_credit(conn, week_start_date):
|
|
2455
|
+
"""--force scope (M2, spec §4): delete ONLY this week's command-owned
|
|
2456
|
+
synthetic snapshots (`source='record-credit'`) + its `weekly_credit_floors`
|
|
2457
|
+
row(s). Never touches real status-line snapshots, and never
|
|
2458
|
+
`week_reset_events` / `percent_milestones` — a partial credit writes
|
|
2459
|
+
neither (no event row -> no segmentation; no milestones)."""
|
|
2460
|
+
conn.execute("DELETE FROM weekly_usage_snapshots "
|
|
2461
|
+
" WHERE week_start_date=? AND source='record-credit'", (week_start_date,))
|
|
2462
|
+
conn.execute("DELETE FROM weekly_credit_floors "
|
|
2463
|
+
" WHERE week_start_date=?", (week_start_date,))
|
|
2464
|
+
conn.commit()
|
|
2465
|
+
|
|
2466
|
+
|
|
2467
|
+
def _count_stale_replays(conn, plan):
|
|
2468
|
+
"""Count the pre-credit replay rows the _apply_credit stale-replay DELETE
|
|
2469
|
+
(step 4c, inline) will touch (captured at/after the credit moment within a
|
|
2470
|
+
1.0pp band of from), for the preview / --json `staleReplaysDeleted` field.
|
|
2471
|
+
Read-only."""
|
|
2472
|
+
row = conn.execute(
|
|
2473
|
+
"SELECT COUNT(*) FROM weekly_usage_snapshots "
|
|
2474
|
+
" WHERE week_start_date = ? "
|
|
2475
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2476
|
+
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2477
|
+
(plan.week_start_date, plan.effective_iso, float(plan.from_pct)),
|
|
2478
|
+
).fetchone()
|
|
2479
|
+
return int(row[0]) if row and row[0] is not None else 0
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
def _credit_preview_text(plan, *, stale_replays, dry_run):
|
|
2483
|
+
"""Human preview (spec §5). Shown before the confirm prompt and as the
|
|
2484
|
+
whole body under --dry-run."""
|
|
2485
|
+
eff_dt = parse_iso_datetime(plan.effective_iso, "effective").astimezone(dt.timezone.utc)
|
|
2486
|
+
cap_dt = parse_iso_datetime(plan.captured_iso, "captured").astimezone(dt.timezone.utc)
|
|
2487
|
+
we_dt = parse_iso_datetime(plan.week_end_at, "week_end").astimezone(dt.timezone.utc)
|
|
2488
|
+
src = {
|
|
2489
|
+
"hwm": "current HWM",
|
|
2490
|
+
"explicit": "explicit",
|
|
2491
|
+
"prior_credit": "prior credit",
|
|
2492
|
+
}.get(plan.from_source, plan.from_source)
|
|
2493
|
+
lines = [
|
|
2494
|
+
"record-credit — weekly in-place credit",
|
|
2495
|
+
f" week: {plan.week_start_date} -> "
|
|
2496
|
+
f"{we_dt.strftime('%Y-%m-%d %H:%M')} UTC",
|
|
2497
|
+
f" from -> to: {plan.from_pct:g}% -> {plan.to_pct:g}% (from: {src})",
|
|
2498
|
+
f" effective: {eff_dt.strftime('%Y-%m-%d %H:%M')} UTC "
|
|
2499
|
+
f"(floored from {cap_dt.strftime('%Y-%m-%d %H:%M')})",
|
|
2500
|
+
" writes:",
|
|
2501
|
+
f" + weekly_credit_floors (effective={plan.effective_iso}, "
|
|
2502
|
+
f"pre_credit={plan.from_pct:g})",
|
|
2503
|
+
f" ~ hwm-7d {plan.from_pct:g} -> {plan.to_pct:g}",
|
|
2504
|
+
f" - stale replays {stale_replays} rows",
|
|
2505
|
+
f" + snapshot captured={plan.captured_iso}, "
|
|
2506
|
+
f"weekly_percent={plan.to_pct:g}",
|
|
2507
|
+
" note: same week — no window re-anchor (no week_reset_events row)",
|
|
2508
|
+
]
|
|
2509
|
+
if dry_run:
|
|
2510
|
+
lines.append(" (dry-run — nothing written)")
|
|
2511
|
+
return "\n".join(lines)
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
def _credit_json(plan, *, applied, dry_run, forced, stale_replays, hwm_before):
|
|
2515
|
+
"""The --json envelope (schemaVersion 1, spec §5); all datetimes …Z."""
|
|
2516
|
+
def _z(iso):
|
|
2517
|
+
return parse_iso_datetime(iso, "z").astimezone(
|
|
2518
|
+
dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
2519
|
+
return {
|
|
2520
|
+
"schemaVersion": 1,
|
|
2521
|
+
"applied": applied,
|
|
2522
|
+
"dryRun": dry_run,
|
|
2523
|
+
"forced": forced,
|
|
2524
|
+
"week": {
|
|
2525
|
+
"weekStartDate": plan.week_start_date,
|
|
2526
|
+
"weekStartAt": _z(plan.week_start_at),
|
|
2527
|
+
"weekEndAt": _z(plan.week_end_at),
|
|
2528
|
+
},
|
|
2529
|
+
"credit": {
|
|
2530
|
+
"fromPct": plan.from_pct,
|
|
2531
|
+
"toPct": plan.to_pct,
|
|
2532
|
+
"fromSource": plan.from_source,
|
|
2533
|
+
"effectiveAtUtc": _z(plan.effective_iso),
|
|
2534
|
+
},
|
|
2535
|
+
"actions": {
|
|
2536
|
+
"creditFloorInserted": applied,
|
|
2537
|
+
"hwm7dBefore": hwm_before,
|
|
2538
|
+
"hwm7dAfter": plan.to_pct if applied else hwm_before,
|
|
2539
|
+
"staleReplaysDeleted": stale_replays,
|
|
2540
|
+
"postCreditSnapshotInserted": applied,
|
|
2541
|
+
},
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
|
|
2545
|
+
def cmd_record_credit(args) -> int:
|
|
2546
|
+
now = _command_as_of()
|
|
2547
|
+
try:
|
|
2548
|
+
at_dt = _parse_credit_at(getattr(args, "at", None), now)
|
|
2549
|
+
except ValueError as e:
|
|
2550
|
+
eprint(f"record-credit: {e}")
|
|
2551
|
+
return 2
|
|
2552
|
+
conn = None
|
|
2553
|
+
try:
|
|
2554
|
+
conn = open_db()
|
|
2555
|
+
# 1. Resolve the week.
|
|
2556
|
+
if getattr(args, "week", None):
|
|
2557
|
+
week_start_date = args.week
|
|
2558
|
+
ws_at, we_at = _get_canonical_boundary_for_date(conn, week_start_date)
|
|
2559
|
+
if not ws_at or not we_at:
|
|
2560
|
+
eprint(f"record-credit: no snapshot for --week {week_start_date}")
|
|
2561
|
+
return 2
|
|
2562
|
+
else:
|
|
2563
|
+
fetched = _fetch_current_week_snapshots(conn, at_dt)
|
|
2564
|
+
if fetched is None:
|
|
2565
|
+
eprint("record-credit: no snapshot week contains --at; pass --week")
|
|
2566
|
+
return 2
|
|
2567
|
+
ws_at, we_at, _samples = fetched
|
|
2568
|
+
ws_at = ws_at if isinstance(ws_at, str) else ws_at.isoformat(timespec="seconds")
|
|
2569
|
+
we_at = we_at if isinstance(we_at, str) else we_at.isoformat(timespec="seconds")
|
|
2570
|
+
week_start_date = parse_iso_datetime(ws_at, "ws_at").date().isoformat()
|
|
2571
|
+
|
|
2572
|
+
# Resolve any existing credit FLOOR for this week up front — needed
|
|
2573
|
+
# both for the --from default fallback (a half-applied credit empties
|
|
2574
|
+
# the post-credit segment, so the reset-aware HWM reads NULL) and the
|
|
2575
|
+
# apply-time completion/refuse/force branch (M2 keys on
|
|
2576
|
+
# weekly_credit_floors, NOT week_reset_events — a partial credit never
|
|
2577
|
+
# writes a reset-event row). Latest floor wins (a --force re-apply at a
|
|
2578
|
+
# new effective leaves the old row only until _force_clear_credit
|
|
2579
|
+
# deletes it; pick the newest defensively).
|
|
2580
|
+
existing = conn.execute(
|
|
2581
|
+
"SELECT id, effective_at_utc, observed_pre_credit_pct "
|
|
2582
|
+
"FROM weekly_credit_floors WHERE week_start_date=? "
|
|
2583
|
+
"ORDER BY unixepoch(effective_at_utc) DESC, id DESC LIMIT 1",
|
|
2584
|
+
(week_start_date,)).fetchone()
|
|
2585
|
+
|
|
2586
|
+
# 2. Resolve --from default.
|
|
2587
|
+
if getattr(args, "from_pct", None) is not None:
|
|
2588
|
+
from_pct, from_source = float(args.from_pct), "explicit"
|
|
2589
|
+
elif existing is not None and existing[2] is not None:
|
|
2590
|
+
# A credit floor already exists for this week (completion or
|
|
2591
|
+
# --force re-record). Its recorded observed_pre_credit_pct is the
|
|
2592
|
+
# AUTHENTIC pre-credit baseline; prefer it over the reset-aware
|
|
2593
|
+
# HWM. The post-credit segment's MAX(weekly_percent) would
|
|
2594
|
+
# otherwise pick up the post-credit value (31) or a later real
|
|
2595
|
+
# status-line reading, mis-deriving the baseline and causing the
|
|
2596
|
+
# stale-replay DELETE to (wrongly) match real history. fromSource
|
|
2597
|
+
# is 'prior_credit' (spec §5).
|
|
2598
|
+
from_pct, from_source = float(existing[2]), "prior_credit"
|
|
2599
|
+
else:
|
|
2600
|
+
hwm = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
2601
|
+
if hwm is None:
|
|
2602
|
+
eprint("record-credit: no usage history for the week; pass --from")
|
|
2603
|
+
return 2
|
|
2604
|
+
from_pct, from_source = hwm, "hwm"
|
|
2605
|
+
|
|
2606
|
+
is_force = getattr(args, "force", False)
|
|
2607
|
+
|
|
2608
|
+
# 2a. Classify the existing-floor state (M2, spec §4/§5). A
|
|
2609
|
+
# `weekly_credit_floors` row may be:
|
|
2610
|
+
# - half-applied (floor row present, NO command-owned snapshot
|
|
2611
|
+
# at/after its effective): a crash between 4a and 4d. A plain
|
|
2612
|
+
# rerun FINISHES it, reusing the existing effective_at_utc (NOT
|
|
2613
|
+
# a fresh floor_to_hour(now)) so no stale [old,new) replay leaks
|
|
2614
|
+
# into the floored MAX (spec §4a completion-effective reuse).
|
|
2615
|
+
# - fully applied (floor row + command-owned snapshot): refuse by
|
|
2616
|
+
# default; --force clears + re-records at a fresh effective.
|
|
2617
|
+
is_completion = False
|
|
2618
|
+
if existing is not None and not is_force:
|
|
2619
|
+
owned = conn.execute(
|
|
2620
|
+
"SELECT 1 FROM weekly_usage_snapshots "
|
|
2621
|
+
" WHERE week_start_date=? AND source='record-credit' "
|
|
2622
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) LIMIT 1",
|
|
2623
|
+
(week_start_date, existing[1])).fetchone()
|
|
2624
|
+
is_completion = owned is None
|
|
2625
|
+
|
|
2626
|
+
# The effective the plan should carry: a half-applied completion reuses
|
|
2627
|
+
# the EXISTING floor row's effective; a first credit / --force re-apply
|
|
2628
|
+
# uses floor_to_hour(at) (computed inside _build_credit_plan).
|
|
2629
|
+
reuse_effective = existing[1] if is_completion else None
|
|
2630
|
+
|
|
2631
|
+
# 3. Validate + build plan.
|
|
2632
|
+
try:
|
|
2633
|
+
plan = _build_credit_plan(
|
|
2634
|
+
week_start_date=week_start_date, week_start_at=ws_at,
|
|
2635
|
+
week_end_at=we_at, from_pct=from_pct, from_source=from_source,
|
|
2636
|
+
to_pct=args.to, at_dt=at_dt, now=now,
|
|
2637
|
+
effective_override=reuse_effective,
|
|
2638
|
+
)
|
|
2639
|
+
except ValueError as e:
|
|
2640
|
+
eprint(f"record-credit: {e}")
|
|
2641
|
+
return 2
|
|
2642
|
+
|
|
2643
|
+
# 4. Output + confirm matrix (spec §5).
|
|
2644
|
+
is_json = getattr(args, "json", False)
|
|
2645
|
+
is_dry = getattr(args, "dry_run", False)
|
|
2646
|
+
is_yes = getattr(args, "yes", False)
|
|
2647
|
+
stale_replays = _count_stale_replays(conn, plan)
|
|
2648
|
+
hwm_before = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
2649
|
+
if hwm_before is None:
|
|
2650
|
+
hwm_before = plan.from_pct
|
|
2651
|
+
|
|
2652
|
+
# --dry-run: preview only, write nothing, exit 0 (TTY or not,
|
|
2653
|
+
# with/without --json).
|
|
2654
|
+
if is_dry:
|
|
2655
|
+
if is_json:
|
|
2656
|
+
print(json.dumps(_credit_json(
|
|
2657
|
+
plan, applied=False, dry_run=True, forced=False,
|
|
2658
|
+
stale_replays=stale_replays, hwm_before=hwm_before)))
|
|
2659
|
+
else:
|
|
2660
|
+
print(_credit_preview_text(plan, stale_replays=stale_replays,
|
|
2661
|
+
dry_run=True))
|
|
2662
|
+
return 0
|
|
2663
|
+
|
|
2664
|
+
# --json (not dry-run) must be paired with --yes; never prompts.
|
|
2665
|
+
if is_json and not is_yes:
|
|
2666
|
+
eprint("record-credit: --json requires --yes or --dry-run")
|
|
2667
|
+
return 2
|
|
2668
|
+
|
|
2669
|
+
# No --yes: prompt (TTY) or refuse (non-TTY).
|
|
2670
|
+
if not is_yes:
|
|
2671
|
+
if not sys.stdin.isatty():
|
|
2672
|
+
eprint("record-credit: stdin not a TTY: pass --yes to apply "
|
|
2673
|
+
"or --dry-run to preview")
|
|
2674
|
+
return 2
|
|
2675
|
+
print(_credit_preview_text(plan, stale_replays=stale_replays,
|
|
2676
|
+
dry_run=False))
|
|
2677
|
+
try:
|
|
2678
|
+
reply = input("Proceed? [y/N] ")
|
|
2679
|
+
except EOFError:
|
|
2680
|
+
reply = ""
|
|
2681
|
+
if reply.strip().lower() not in ("y", "yes"):
|
|
2682
|
+
print("aborted — nothing written")
|
|
2683
|
+
return 0
|
|
2684
|
+
|
|
2685
|
+
# 4a. Existing-floor handling (M2; state classified at step 2a).
|
|
2686
|
+
# _apply_credit commits the floor row + cleanup BEFORE the synthetic
|
|
2687
|
+
# snapshot, so a crash in between leaves a half-applied credit
|
|
2688
|
+
# (floor row, no command-owned snapshot). `existing` /
|
|
2689
|
+
# `is_completion` were resolved up front (above) keyed on
|
|
2690
|
+
# week_start_date / weekly_credit_floors.
|
|
2691
|
+
forced = False
|
|
2692
|
+
if existing is not None and not is_force:
|
|
2693
|
+
if not is_completion:
|
|
2694
|
+
# Fully applied -> refuse by default.
|
|
2695
|
+
eprint(f"record-credit: a credit is already recorded for this "
|
|
2696
|
+
f"week (effective={existing[1]}, pre_credit={existing[2]}); "
|
|
2697
|
+
f"pass --force to re-record")
|
|
2698
|
+
return 2
|
|
2699
|
+
# else: half-applied -> fall through (completion path; the plan
|
|
2700
|
+
# reuses the existing effective and the apply steps are idempotent).
|
|
2701
|
+
elif existing is not None and is_force:
|
|
2702
|
+
_force_clear_credit(conn, plan.week_start_date)
|
|
2703
|
+
forced = True
|
|
2704
|
+
|
|
2705
|
+
five_hour = _resolve_prior_5h(conn, at_dt)
|
|
2706
|
+
_apply_credit(conn, plan, five_hour=five_hour)
|
|
2707
|
+
|
|
2708
|
+
if is_json:
|
|
2709
|
+
print(json.dumps(_credit_json(
|
|
2710
|
+
plan, applied=True, dry_run=False, forced=forced,
|
|
2711
|
+
stale_replays=stale_replays, hwm_before=hwm_before)))
|
|
2712
|
+
else:
|
|
2713
|
+
print(f"record-credit: applied — week {plan.week_start_date} "
|
|
2714
|
+
f"{plan.from_pct:g}% -> {plan.to_pct:g}% "
|
|
2715
|
+
f"(effective {plan.effective_iso})")
|
|
2716
|
+
return 0
|
|
2717
|
+
except sqlite3.DatabaseError as e:
|
|
2718
|
+
# Documented exit 3 (docs/commands/record-credit.md "3 — a database
|
|
2719
|
+
# error"). The inner ValueError->2 / EOFError paths return before
|
|
2720
|
+
# reaching here, so this only catches genuine DB failures from
|
|
2721
|
+
# open_db() through _apply_credit/output. Plain-text on stderr,
|
|
2722
|
+
# matching the record-credit: <msg> convention of the validation paths.
|
|
2723
|
+
eprint(f"record-credit: {e}")
|
|
2724
|
+
return 3
|
|
2725
|
+
finally:
|
|
2726
|
+
if conn is not None:
|
|
2727
|
+
conn.close()
|
|
2728
|
+
|
|
2729
|
+
|
|
2241
2730
|
def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
2242
2731
|
"""Record usage data from Claude Code status line rate_limits."""
|
|
2243
2732
|
c = _cctally()
|
|
@@ -2758,38 +3247,43 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2758
3247
|
eprint(f"[record-usage] reset-event detection failed: {exc}")
|
|
2759
3248
|
|
|
2760
3249
|
# 7-day usage is monotonically non-decreasing within a billing week
|
|
2761
|
-
# — UNTIL
|
|
2762
|
-
#
|
|
2763
|
-
#
|
|
2764
|
-
#
|
|
2765
|
-
#
|
|
2766
|
-
#
|
|
2767
|
-
#
|
|
2768
|
-
#
|
|
3250
|
+
# — UNTIL an in-place weekly credit lowers it. There are TWO credit
|
|
3251
|
+
# shapes and BOTH must floor this clamp, or a real post-credit tick
|
|
3252
|
+
# is suppressed and never stored:
|
|
3253
|
+
# (a) an Anthropic mid-week reset / >=25pp auto-credit writes a
|
|
3254
|
+
# `week_reset_events` row (it also re-anchors the window); and
|
|
3255
|
+
# (b) a manual `record-credit` partial credit writes a
|
|
3256
|
+
# `weekly_credit_floors` row WITHOUT re-anchoring the week
|
|
3257
|
+
# (record-credit M2, #209).
|
|
3258
|
+
# `_reset_aware_floor` returns the LATEST in-week effective across
|
|
3259
|
+
# both legs. The MAX query then filters to samples captured at-or-
|
|
3260
|
+
# after that floor, so a fresh post-credit OAuth value (e.g. 37%
|
|
3261
|
+
# after a 46->31 credit) lands instead of being held back by stale
|
|
3262
|
+
# pre-credit history (46%). Without the credit-floor leg, the 37%
|
|
3263
|
+
# tick is `round(37,1) < round(46,1)` -> should_insert=False ->
|
|
3264
|
+
# never stored, cascading to every latest-snapshot surface
|
|
3265
|
+
# (the M2 linchpin; spec §4a, test S13).
|
|
3266
|
+
# When neither leg has a row, the floor is None -> '1970-...' epoch-
|
|
3267
|
+
# zero default -> the filter is a no-op and legacy clamp behavior is
|
|
3268
|
+
# preserved byte-identically.
|
|
2769
3269
|
# NB: comparison wrapped with ``unixepoch()`` on BOTH sides.
|
|
2770
|
-
# ``captured_at_utc`` is stored with `Z` suffix, but
|
|
2771
|
-
#
|
|
2772
|
-
#
|
|
2773
|
-
# (
|
|
2774
|
-
#
|
|
2775
|
-
#
|
|
2776
|
-
|
|
2777
|
-
|
|
3270
|
+
# ``captured_at_utc`` is stored with `Z` suffix, but the floor may
|
|
3271
|
+
# carry a non-UTC / +00:00 offset spelling. Lex string compare on
|
|
3272
|
+
# mixed offsets silently mis-orders moments for non-UTC hosts
|
|
3273
|
+
# (CLAUDE.md gotcha: 5h-block cross-reset flag — "all comparisons go
|
|
3274
|
+
# through unixepoch(), NOT lex BETWEEN/`<`/`>`"). Same rule here, and
|
|
3275
|
+
# inside `_reset_aware_floor`'s own ORDER BY.
|
|
3276
|
+
clamp_floor_iso = _reset_aware_floor(
|
|
3277
|
+
conn, week_start_date, week_start_at, week_end_at,
|
|
3278
|
+
) or "1970-01-01T00:00:00Z"
|
|
2778
3279
|
max_row = conn.execute(
|
|
2779
3280
|
"""
|
|
2780
3281
|
SELECT MAX(weekly_percent) AS v
|
|
2781
3282
|
FROM weekly_usage_snapshots
|
|
2782
3283
|
WHERE week_start_date = ?
|
|
2783
|
-
AND unixepoch(captured_at_utc) >= unixepoch(
|
|
2784
|
-
(SELECT effective_reset_at_utc
|
|
2785
|
-
FROM week_reset_events
|
|
2786
|
-
WHERE new_week_end_at = ?
|
|
2787
|
-
ORDER BY id DESC
|
|
2788
|
-
LIMIT 1),
|
|
2789
|
-
'1970-01-01T00:00:00Z'
|
|
2790
|
-
))
|
|
3284
|
+
AND unixepoch(captured_at_utc) >= unixepoch(?)
|
|
2791
3285
|
""",
|
|
2792
|
-
(week_start_date,
|
|
3286
|
+
(week_start_date, clamp_floor_iso),
|
|
2793
3287
|
).fetchone()
|
|
2794
3288
|
if max_row and max_row["v"] is not None and round(weekly_percent, 1) < round(float(max_row["v"]), 1):
|
|
2795
3289
|
should_insert = False
|
|
@@ -500,20 +500,22 @@ def _build_statusline_injections(warn_once):
|
|
|
500
500
|
# (_apply_reset_events_to_subweeks: post-reset window
|
|
501
501
|
# start_ts := effective_reset_at_utc) by flooring the
|
|
502
502
|
# MAX to snapshots captured at/after the latest reset
|
|
503
|
-
# effective WITHIN this window.
|
|
504
|
-
#
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
503
|
+
# effective WITHIN this window.
|
|
504
|
+
#
|
|
505
|
+
# The floor is the LATEST in-week effective across BOTH
|
|
506
|
+
# `week_reset_events` (Anthropic resets / >=25pp auto-
|
|
507
|
+
# credits) and `weekly_credit_floors` (manual `record-
|
|
508
|
+
# credit` partial credits — record-credit M2, #209): a
|
|
509
|
+
# partial credit lowers the clamp floor WITHOUT re-
|
|
510
|
+
# anchoring the week, so without the credit-floor leg the
|
|
511
|
+
# statusline would re-clamp the post-credit 31% back UP to
|
|
512
|
+
# the stale pre-credit 46% peak. `_reset_aware_floor`
|
|
513
|
+
# unions both legs with `unixepoch()` ordering (mixed
|
|
514
|
+
# Z / +00:00 offset spellings; lexical MAX would misorder
|
|
515
|
+
# them — same rule as the 5h-block cross-reset flag).
|
|
516
|
+
floor_iso = c._reset_aware_floor(
|
|
517
|
+
conn, week_start_date,
|
|
518
|
+
week_start_dt.isoformat(), week_end_dt.isoformat(),
|
|
517
519
|
)
|
|
518
520
|
if floor_iso is not None:
|
|
519
521
|
row = conn.execute(
|
package/bin/cctally
CHANGED
|
@@ -212,6 +212,7 @@ WeekRef = _cctally_core.WeekRef
|
|
|
212
212
|
_canonicalize_optional_iso = _cctally_core._canonicalize_optional_iso
|
|
213
213
|
make_week_ref = _cctally_core.make_week_ref
|
|
214
214
|
_get_latest_row_for_week = _cctally_core._get_latest_row_for_week
|
|
215
|
+
_reset_aware_floor = _cctally_core._reset_aware_floor
|
|
215
216
|
get_latest_usage_for_week = _cctally_core.get_latest_usage_for_week
|
|
216
217
|
|
|
217
218
|
# === Path constants — re-exported from _cctally_core ================
|
|
@@ -634,6 +635,9 @@ cmd_sync_week = _cctally_sync_week.cmd_sync_week
|
|
|
634
635
|
# module-private (no external caller).
|
|
635
636
|
_cctally_project = _load_sibling("_cctally_project")
|
|
636
637
|
cmd_project = _cctally_project.cmd_project
|
|
638
|
+
# Re-exported so the record-credit tests can assert `project`'s per-week MAX
|
|
639
|
+
# is reset-aware-floored (S15, #209).
|
|
640
|
+
_load_week_snapshots = _cctally_project._load_week_snapshots
|
|
637
641
|
# Shared per-project cost compute (#19/#121, spec §7.1). Re-exported so the
|
|
638
642
|
# per-project budget display (cmd_budget) and the Task 3 firing path reach it
|
|
639
643
|
# via the cctally namespace.
|
|
@@ -840,6 +844,22 @@ _weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projectio
|
|
|
840
844
|
_compute_block_totals = _cctally_record._compute_block_totals
|
|
841
845
|
maybe_update_five_hour_block = _cctally_record.maybe_update_five_hour_block
|
|
842
846
|
cmd_record_usage = _cctally_record.cmd_record_usage
|
|
847
|
+
CreditPlan = _cctally_record.CreditPlan
|
|
848
|
+
_parse_credit_at = _cctally_record._parse_credit_at
|
|
849
|
+
_build_credit_plan = _cctally_record._build_credit_plan
|
|
850
|
+
_resolve_reset_aware_hwm = _cctally_record._resolve_reset_aware_hwm
|
|
851
|
+
_insert_credit_snapshot = _cctally_record._insert_credit_snapshot
|
|
852
|
+
_resolve_prior_5h = _cctally_record._resolve_prior_5h
|
|
853
|
+
_apply_credit = _cctally_record._apply_credit
|
|
854
|
+
_fire_in_place_credit = _cctally_record._fire_in_place_credit
|
|
855
|
+
_force_clear_credit = _cctally_record._force_clear_credit
|
|
856
|
+
_count_stale_replays = _cctally_record._count_stale_replays
|
|
857
|
+
_credit_preview_text = _cctally_record._credit_preview_text
|
|
858
|
+
_credit_json = _cctally_record._credit_json
|
|
859
|
+
_arm_reset_zero_marker = _cctally_record._arm_reset_zero_marker
|
|
860
|
+
_read_reset_zero_marker = _cctally_record._read_reset_zero_marker
|
|
861
|
+
_clear_reset_zero_marker = _cctally_record._clear_reset_zero_marker
|
|
862
|
+
cmd_record_credit = _cctally_record.cmd_record_credit
|
|
843
863
|
_hook_tick_log_line = _cctally_record._hook_tick_log_line
|
|
844
864
|
_hook_tick_log_rotate_if_needed = _cctally_record._hook_tick_log_rotate_if_needed
|
|
845
865
|
_hook_tick_throttle_age_seconds = _cctally_record._hook_tick_throttle_age_seconds
|