ouroboros/scripts/export_usage_journal.py
Ouroboros 959de027b3 Make state/usage.sqlite the one money authority, read by address
The usage ledger was an append-only JSONL file folded whole by every process: each
of the 41 processes parsed it at first use and again after every compaction swap,
any append dropped every cached render, and a single-key question re-summed every
live final row. Serving the readers from addressed records after reopen needed
persisted per-key records, which the accepted design criterion resolves as one
relational store.

ouroboros/usage_store.py owns state/usage.sqlite: one row per attempt, updated on
each transition under a revision check; summaries per global, root, group, task,
provider, model, category and their unattributed and per-root/per-task axes,
maintained in the writing transaction by subtracting the previous row's
contribution from its keys and adding the new row's to the new keys, with the
reducer the old fold used (cash as exact decimal text in Python arithmetic, counts
and token presence weighted by an imported aggregate's folded count, cash and
tokens added once); the earliest binding per root and group; dirty owners for the
maintenance duty; one-shot identities; the publication marker continued from the
journal. Admission reads two or three summary rows; displays read the same rows and
report an unavailable fact after the short wait instead of serving a stale copy;
dispatch rechecks known spend and the pause fence; the acquisition slices that check
Stop and deadlines stay around the short transactions.

Two lock tiers, decided once at import and recorded in the store: SQLite's own locks
where the kernel enforces them, and no SQLite locking under the existing name-tier
money lock on Drive, FUSE and NFS roots (durability there stays best effort, as the
journal's appends were). The migration is a visible startup phase before any worker:
it imports the last row of every attempt, aggregates with their weights, bindings and
provenance, commits, marks the import complete, then renames the journal to
state/usage_attempts.jsonl.imported; the archive stays on disk and is read only by
the explicit seal audit. scripts/export_usage_journal.py writes a journal the previous
validator accepts for a downgrade.

Deleted with their replacements: usage compaction (the archive chain, anchors,
segment cache and growth floors), the writer-view memo with its generation checks and
stale-display backoff, the journal's tail quarantine, dense-sequence validation and
incremental resume, the legacy import module, docs/USAGE_COMPACTION.md and the dead
outputs. docs/USAGE_STORE.md describes the store.
2026-10-05 14:11:37 +03:00

37 lines
1.3 KiB
Python

#!/usr/bin/env python3
"""Export the usage store as the journal an older Ouroboros release reads.
Usage: python scripts/export_usage_journal.py <data_root>
Run it OFFLINE, with the Ouroboros server stopped, as the step before checking
out a release older than the usage store (docs/USAGE_STORE.md "Downgrade"). It
writes ``state/usage_attempts.jsonl`` from ``state/usage.sqlite`` (a chain the
older validator accepts: each attempt's minimal legal transition chain ending in
its current row, imported aggregates under their baseline header, dense seq),
aligns the ``state.json`` freshness marker with it, and renames the store aside
so the next upgrade imports the journal again. A git revert alone is not a data
rollback. Prints one JSON report.
"""
from __future__ import annotations
import json
import pathlib
import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
def main(argv: list[str]) -> int:
if len(argv) != 1:
print(__doc__, file=sys.stderr)
return 2
root = pathlib.Path(argv[0]).expanduser().resolve()
from ouroboros.usage_store import export_journal
print(json.dumps(export_journal(root), sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))