toolsThe CLI that owns the ledger's vocabulary and semantics: read, append one observation, refresh the row's latest scalars, write atomically, regenerate views, report drift.
BASE_FIELDS, HISTORY_FIELDS).REAL_OUTCOMES) and the stage ladder (STAGES).0600.tools/ledger.py — everything above, 276 lines.load() — whole store as a dict, {} when the file does not exist yet.save(d) — realpath → temp file → os.replace → chmod 0600.blank_row(ip) — the field set for a new address; projects and history start empty, attempts at 0.drift_check(d) — sorted list of field names the tool does not know. Reports only.is_reusable(r) — true when session_tag and url_template are present, and country_code is present when the template contains {cc}.real_observation(r) — true when any history entry (or the scalar, for rows without history) carries an outcome in REAL_OUTCOMES.cmd_observe, cmd_views, cmd_audit, main.BASE_FIELDS — `ip · provider · endpoint · session_tag · country_code · url_template · cred_ref · country · isp · first_seen · last_seen · projects · stage_reached · failed_at · outcome · reason · attempts`HISTORY_FIELDS — `at · project · stage_reached · outcome · failed_at · reason · attempts · provider · endpoint · session_tag · country_code · source`STAGES (the ladder) — `probe → page_load → challenge_issued → token_harvested → submitted → account_created`REAL_OUTCOMES — success · promising · blocked · captcha_failed · transientLEDGER resolves from $RIME_IP_LEDGER, falling back to ; VIEWS is always . Each observe call rewrites the whole file — measured at 5.6 ms per write for a few hundred observations, which is fine at this scale and a reason to move to SQLite if the store ever holds thousands.
outlook-creator, grok_cauldron) through its CLI, and by anyone reading views/.os, sys, json, argparse, datetime.os.replace on a symlink replaces the link. save() therefore writes to os.path.realpath(LEDGER). Removing that single line is what produced two disagreeing ledgers on 21 Sep 2026.outcome is not a summary of the IP, it is the most recent word — history is the only place the whole story lives.failed is not in REAL_OUTCOMES. Three rows currently carry it, so real_observation() calls them unused and they surface as FRESH. Either the outcome vocabulary grows to include failed, or the writers stop emitting it — but silently treating a failure as "never spent" is the one option that is wrong.load() reads the entire store per call. Every command is O(store) in memory; that is deliberate at this size, and it is the constraint to watch.