diff --git a/DURABLE_STATE.md b/DURABLE_STATE.md new file mode 100644 index 0000000000000000000000000000000000000000..bb67a16958d981c225826f26f4c46784c380b4bb --- /dev/null +++ b/DURABLE_STATE.md @@ -0,0 +1,107 @@ +# Durable constitution state + +The live application reads and writes `/data/constitution.rocks`. The historical +`ledger.jsonl` remains the archival tape and migration contract; the running +application never appends to or replays it. + +## Dependency + +`pyproject.toml` pins `evaleval` to the immutable Git commit containing the +durable state API. Regenerate `uv.lock` normally when changing that pin; do not +use a local path dependency or hand-edit the lock. + +## Import modes + +The preferred rollout path is an opt-in synchronous first boot: + +```sh +IMPORT_JSONL_ON_EMPTY=1 \ +JSONL_PATH=/data/ledger.jsonl \ +ROCKS_PATH=/data/constitution.rocks \ +python constitution.py +``` + +Before the server starts accepting traffic, it closes its RocksDB probe, +streams the tape into an empty projection, verifies it, then opens the live +database. If the database is nonempty, it is opened unchanged and the tape is +not imported. A missing tape or failed import fails startup. During import the +HTTP health endpoint is not yet available; after startup `/api/health` reports +`ledger_ready`, `importing`, `error`, and the indexed event count. + +The manual importer remains available in the image for maintenance downtime: + +During maintenance downtime: + +```sh +python scripts/import-ledger.py \ + /data/ledger.jsonl \ + /data/constitution.rocks \ + --batch-size 100 +``` + +Use `--force` to destroy an existing projection and rebuild from byte zero. +Import reads one JSONL line at a time, commits bounded batches, verifies event +and type counts, chain head, and representative exact lookups, and deletes an +incomplete Rocks projection on failure. + +Start the application with: + +```sh +ROCKS_PATH=/data/constitution.rocks python constitution.py +``` + +Do not run the importer concurrently with the application. + +## Rollout sequence + +1. Verify the immutable `evaleval` pin, regenerate `uv.lock`, build the image, + and run both repositories' complete suites. +2. Keep `DISABLE_EPOCH_LOOP=1`. Take a volume snapshot/backup containing + `/data/ledger.jsonl`. Do not unpause writes during migration. +3. Add `IMPORT_JSONL_ON_EMPTY=1` and deploy with a non-overlapping/immediate + strategy so the old process releases the volume before the new process + probes RocksDB: + + ```sh + fly deploy --strategy immediate + ``` + +4. Wait for startup/import to finish. Require `/api/health` to report + `ok=true`, `ledger_ready=true`, `importing=false`, `error=null`, and the + expected `event_count`. Verify representative epoch, evidence hardlink, + patch download, ranking, latest discovery, and latest emission routes. +5. Remove `IMPORT_JSONL_ON_EMPTY` and deploy again while + `DISABLE_EPOCH_LOOP=1`. A nonempty RocksDB would be preserved either way; + removing the flag makes normal operation explicit. +6. After the second health and route check, remove + `DISABLE_EPOCH_LOOP` and deploy once more to unpause. + +Rollback before step 6 is safe: stop the Rocks-backed process, deploy the prior +image with `DISABLE_EPOCH_LOOP=1`, and continue using the unchanged archival +JSONL tape. Keep the Rocks directory for diagnosis. After step 6, the JSONL +tape no longer contains new live writes, so rolling back to the JSONL-writing +version would lose or fork those events. After unpause, rollback must stay on a +Rocks-capable image or first perform an explicitly designed/exported +reconciliation; do not simply start the old writer. + +## Direct schema + +`constitution.py` defines one `Record` rooted at `constitution-v1`: + +- `events`: canonical tape order +- `evidence_by_id`, `event_ids_by_kind`, `event_ids_by_epoch` +- exact entity hardlinks for commits, comparisons, attempts, judgments, + ranking runs, snapshots, and OIDs +- judgments/attempts by comparison and comparisons by commit +- emissions and discoveries by epoch plus exact latest values +- durable Git OID and patch-identity discovery indexes +- content-addressed raw blobs by SHA-256 +- chain head, emitted total, import count, and tape digest metadata + +Application code selects these paths directly. `append_evidence` is intentionally +narrow: it exists because canonical append, chain head, blobs, and all secondary +indexes must change atomically. + +New patch/prompt/request/response/diff bytes are stored once under their SHA-256. +Imported legacy envelopes remain unchanged with embedded base64. Detail and +download routes resolve either representation transparently. diff --git a/Dockerfile b/Dockerfile index c9a5c00782371c19ad5ab5c58cf6f5a8ffec0141..02d59875814dc7bf3d5d1d64812c5978ec16c76d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-install-project COPY constitution.py ./ +COPY scripts/import-ledger.py ./scripts/import-ledger.py ENV PATH="/app/.venv/bin:${PATH}" \ PYTHONUNBUFFERED="1" diff --git a/constitution.py b/constitution.py index 17e322a8884a09d7579676ae221b705f46039f6e..384a2cb18181f54ec661f719ec048254ce3df706 100644 --- a/constitution.py +++ b/constitution.py @@ -6,7 +6,8 @@ # "uvicorn", # "httpx", # "tenacity", -# "evaleval==0.2.7", +# "evaleval @ git+https://github.com/tommy-mor/evaleval.git@584225b43f37261b446ad04169aaddf77ca6c201", +# "rocksdict>=0.3.29", # "authlib", # "itsdangerous", # "starlette", @@ -34,9 +35,11 @@ import json, time, os, asyncio, httpx, pathlib, subprocess, hashlib, re, fcntl, import sympy as sp # type: ignore[reportMissingImports] from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from evaleval import ( - event, JsonlStore, to_dict, render, RawContent, Signer, SnippetExecutionError, - exec_event, One, Two, Three, Selector, MORPH, PREPEND, + event, to_dict, from_dict, render, RawContent, Signer, + SnippetExecutionError, exec_event, Selector, MORPH, PREPEND, + Record, Leaf, Map, List, Deque, Sum, RocksDb, Durability, ) +from evaleval.depth import One, Two, Three DefaultContext.prec = 50 getcontext().prec = 50 @@ -113,6 +116,9 @@ CONTRIBUTOR_POOL = TOTAL_SUPPLY - LP_TOKENS GENESIS_MS = int(os.environ["GENESIS_MS"]) JSONL_PATH = pathlib.Path(os.environ.get("JSONL_PATH", "/data/ledger.jsonl")) +ROCKS_PATH = pathlib.Path( + os.environ.get("ROCKS_PATH", "/data/constitution.rocks") +) # =========================================================================== # §1c. CONFIGURATION — environment variables and constants @@ -270,43 +276,97 @@ class Evidence: payload: dict -class DeferredJsonlStore: - """ - Delay ledger replay until after the HTTP server binds. - - A 450MB+ evidence ledger can take minutes to parse; doing that at import - time makes Fly health checks and the public site time out on every boot. - """ - - def __init__(self, path: pathlib.Path | str): - self.path = pathlib.Path(path) - self._inner: JsonlStore | None = None - self.ready = False - - def load_sync(self) -> None: - self._inner = JsonlStore(self.path) - self.ready = True +# Direct application schema. Paths are the query/mutation interface; values are +# plain dictionaries compatible with the unchanged JSONL/CBOR tape contract. +STATE = Record( + events=List(Leaf()), # canonical tape order + evidence_by_id=Map(Leaf()), # event_id -> evidence dict + event_ids_by_kind=Map(List(Leaf())), # kind -> ordered event ids + event_ids_by_epoch=Map(List(Leaf())), # epoch -> ordered event ids + evidence_count_by_epoch=Map(Sum()), + discovery_evidence_by_epoch=Map(Leaf()), + entity_event_ids=Map(Map(Leaf())), # entity field -> id -> event id + evidence_by_entity=Map(Leaf()), # (kind, field, id) -> event id + judgment_ids_by_comparison=Map(List(Leaf())), + judgment_id_by_attempt=Map(Leaf()), + attempt_ids_by_comparison=Map(List(Leaf())), + comparison_ids_by_commit=Map(List(Leaf())), + emissions_by_epoch=Map(Leaf()), + emission_epochs=Map(Leaf()), + latest_emission=Leaf(), + discoveries_by_epoch=Map(Leaf()), + discovery_epochs=Map(Leaf()), + latest_discovery=Leaf(), + epochs=Map(Leaf()), + blobs=Map(Leaf()), # sha256 -> raw bytes + seen_git_oids=Map(Leaf()), + canonical_patch_oids=Map(Leaf()), + meta=Record( + chain_head=Leaf(), + total_emitted=Leaf(), + imported_count=Leaf(), + tape_sha256=Leaf(), + ), +) +ROOT = STATE.root("constitution-v1") +state_db: RocksDb | None = None +_LEDGER_LOCK = asyncio.Lock() +STATE_STATUS = { + "ready": False, + "importing": False, + "error": None, + "event_count": 0, +} - def _require(self) -> JsonlStore: - if self._inner is None: - self.load_sync() - assert self._inner is not None - return self._inner - def read(self) -> list: - if self._inner is None: - return [] - return self._inner.read() +def _db() -> RocksDb: + global state_db + if state_db is None: + state_db = RocksDb.open(ROCKS_PATH) + return state_db - async def append(self, e) -> None: - await self._require().append(e) - async def atomic(self, fn): - return await self._require().atomic(fn) +def prepare_state_sync() -> dict: + """Open live state, optionally importing an archival tape first. + The probe handle is always closed before the importer opens RocksDB. A + nonempty projection is opened as-is and is never overwritten, even when + ``IMPORT_JSONL_ON_EMPTY=1``. + """ + global state_db + if state_db is not None: + count = ROOT.events.len(state_db) + STATE_STATUS.update( + ready=True, importing=False, error=None, event_count=count + ) + return dict(STATE_STATUS) + + STATE_STATUS.update(ready=False, importing=False, error=None, event_count=0) + probe = RocksDb.open(ROCKS_PATH) + count = ROOT.events.len(probe) + probe.close() + + should_import = os.environ.get("IMPORT_JSONL_ON_EMPTY") == "1" + if count == 0 and should_import: + if not JSONL_PATH.is_file(): + message = f"archival tape not found: {JSONL_PATH}" + STATE_STATUS["error"] = message + raise RuntimeError(message) + STATE_STATUS["importing"] = True + try: + import_jsonl_tape(JSONL_PATH, ROCKS_PATH) + except BaseException as exc: + STATE_STATUS.update(importing=False, error=str(exc)) + raise + finally: + STATE_STATUS["importing"] = False -store = DeferredJsonlStore(JSONL_PATH) -_LEDGER_LOCK = asyncio.Lock() + state_db = RocksDb.open(ROCKS_PATH) + count = ROOT.events.len(state_db) + STATE_STATUS.update( + ready=True, importing=False, error=None, event_count=count + ) + return dict(STATE_STATUS) # =========================================================================== @@ -340,9 +400,73 @@ def _bytes_blob(data: bytes | str) -> dict: def _decode_blob(blob: dict | None) -> bytes: if not blob: return b"" + if blob.get("encoding") == "blob" and blob.get("sha256"): + return ROOT.blobs.key(blob["sha256"]).get(_db()) or b"" return base64.b64decode(blob["data"].encode("ascii")) +def _externalize_blob(blob: dict) -> tuple[dict, tuple[str, bytes] | None]: + """Replace an inline base64 blob with a content-addressed reference.""" + if blob.get("encoding") != "base64" or not isinstance(blob.get("data"), str): + return blob, None + raw = base64.b64decode(blob["data"].encode("ascii")) + sha = blob.get("sha256") or _sha256_hex(raw) + return { + "encoding": "blob", + "sha256": sha, + "byte_length": len(raw), + }, (sha, raw) + + +_HEAVY_BLOB_FIELDS = {"patch", "prompt", "request", "response", "diff"} + + +def _externalize_payload(payload: dict) -> tuple[dict, list[tuple[str, bytes]]]: + """Externalize new heavy fields recursively; imported envelopes stay intact.""" + blobs: list[tuple[str, bytes]] = [] + + def walk(value, field: str | None = None): + if field in _HEAVY_BLOB_FIELDS and isinstance(value, (str, bytes)): + raw = value.encode("utf-8") if isinstance(value, str) else value + sha = _sha256_hex(raw) + blobs.append((sha, raw)) + return { + "encoding": "blob", + "sha256": sha, + "byte_length": len(raw), + } + if isinstance(value, dict): + if field in _HEAVY_BLOB_FIELDS: + ref, blob = _externalize_blob(value) + if blob: + blobs.append(blob) + return ref + return {key: walk(child, key) for key, child in value.items()} + if isinstance(value, list): + return [walk(child) for child in value] + return value + + return walk(payload), blobs + + +def _hydrate_heavy_fields(value, field: str | None = None): + """Restore ref-backed text fields for computation-heavy domain objects.""" + if ( + field in _HEAVY_BLOB_FIELDS + and isinstance(value, dict) + and value.get("encoding") == "blob" + ): + return _decode_blob(value).decode("utf-8", "replace") + if isinstance(value, dict): + return { + key: _hydrate_heavy_fields(child, key) + for key, child in value.items() + } + if isinstance(value, list): + return [_hydrate_heavy_fields(child) for child in value] + return value + + def _content_id(prefix: str, material) -> str: digest = _sha256_hex(_canonical_json(material) if not isinstance(material, bytes) else material) return f"{prefix}_{digest}" @@ -373,13 +497,12 @@ def _evidence_url(kind: str, entity_id: str) -> str: return PUBLIC_BASE_URL + _evidence_path(kind, entity_id) -def _previous_event_sha256(events: list) -> str: - for event_ in reversed(events): - if isinstance(event_, Evidence): - return event_.event_id.split("_", 1)[-1] - if isinstance(event_, (GitDiscovery, Emission)): - return _sha256_hex(_canonical_json(to_dict(event_))) - return "0" * 64 +def _chain_digest(row: dict) -> str | None: + if row.get("type") == "evidence": + return str(row["event_id"]).split("_", 1)[-1] + if row.get("type") in {"gitdiscovery", "emission"}: + return _sha256_hex(_canonical_json(row)) + return None def _public_repo_row(repo: dict) -> dict: @@ -395,11 +518,335 @@ def _ledger_lock_path() -> pathlib.Path: env = os.environ.get("LEDGER_LOCK_PATH") if env: return pathlib.Path(env) - return pathlib.Path(str(store.path) + ".lock") + return pathlib.Path(str(ROCKS_PATH) + ".lock") + + +_ENTITY_FIELDS = ( + "commit_id", "comparison_id", "attempt_id", "judgment_id", + "ranking_run_id", "snapshot_id", "oid", +) + + +def _event_writes(row: dict, *, blobs: list[tuple[str, bytes]] | None = None) -> list: + """Lower one tape row to canonical storage plus all direct indexes.""" + writes = [ROOT.events.push(row)] + event_type = row.get("type") + epoch = row.get("epoch") + if isinstance(epoch, int): + writes.append(ROOT.epochs.key(epoch).set(True)) + + if event_type == "evidence": + event_id = row["event_id"] + kind = row["kind"] + payload = row.get("payload") or {} + writes.extend([ + ROOT.evidence_by_id.key(event_id).set(row), + ROOT.event_ids_by_kind.key(kind).push(event_id), + ROOT.event_ids_by_epoch.key(int(row["epoch"])).push(event_id), + ROOT.evidence_count_by_epoch.key(int(row["epoch"])).add(1), + ]) + if kind == "git.discovery_completed": + writes.append( + ROOT.discovery_evidence_by_epoch + .key(int(row["epoch"])).set(event_id) + ) + for field in _ENTITY_FIELDS: + value = payload.get(field) + if value is not None: + writes.extend([ + ROOT.entity_event_ids.key(field).key(str(value)).set(event_id), + ROOT.evidence_by_entity + .key((kind, field, str(value))).set(event_id), + ]) + comparison_id = payload.get("comparison_id") + if kind == "llm.judgment" and comparison_id: + writes.append( + ROOT.judgment_ids_by_comparison + .key(str(comparison_id)).push(event_id) + ) + if payload.get("attempt_id"): + writes.append( + ROOT.judgment_id_by_attempt + .key(str(payload["attempt_id"])).set(event_id) + ) + if kind.startswith("llm.attempt_") and comparison_id: + writes.append( + ROOT.attempt_ids_by_comparison + .key(str(comparison_id)).push(event_id) + ) + if kind == "comparison.input": + comparison_id = payload.get("comparison_id") + if comparison_id: + for side_name in ("side_a", "side_b"): + side = payload.get(side_name) or {} + commit_ids = { + side.get("commit_id"), + *(side.get("commit_ids") or []), + } + for commit_id in commit_ids - {None}: + writes.append( + ROOT.comparison_ids_by_commit + .key(str(commit_id)).push(str(comparison_id)) + ) + + elif event_type == "emission": + epoch = int(row["epoch"]) + writes.extend([ + ROOT.emissions_by_epoch.key(epoch).set(row), + ROOT.emission_epochs.key(epoch).set(True), + ROOT.latest_emission.set(row), + ROOT.meta.total_emitted.set( + str(CONTRIBUTOR_POOL - Decimal(row["pool_after"])) + ), + ]) + + elif event_type == "gitdiscovery": + epoch = int(row["epoch"]) + writes.extend([ + ROOT.discoveries_by_epoch.key(epoch).set(row), + ROOT.discovery_epochs.key(epoch).set(True), + ROOT.latest_discovery.set(row), + ]) + for observation in row.get("observations") or []: + oid = observation.get("oid") + if oid: + writes.append(ROOT.seen_git_oids.key(oid).set(True)) + patch_identity = observation.get("patch_identity") + canonical_oid = observation.get("canonical_patch_oid") + if patch_identity and canonical_oid: + writes.append( + ROOT.canonical_patch_oids.key(patch_identity).set(canonical_oid) + ) + + digest = _chain_digest(row) + if digest: + writes.append(ROOT.meta.chain_head.set(digest)) + for sha, raw in blobs or []: + writes.append(ROOT.blobs.key(sha).set(raw)) + return writes + + +def _typed(row: dict | None): + return from_dict(row) if row else None + + +def _evidence_from_ids(ids: list[str]) -> list[Evidence]: + rows = [] + db = _db() + for event_id in ids: + event_ = _typed(ROOT.evidence_by_id.key(event_id).get(db)) + if isinstance(event_, Evidence): + rows.append(event_) + return rows + + +def _evidence_for_kind(kind: str) -> list[Evidence]: + path = ROOT.event_ids_by_kind.key(kind) + return _evidence_from_ids(path.iter(_db()) if path.len(_db()) else []) + + +def _evidence_for_epoch(epoch: int) -> list[Evidence]: + path = ROOT.event_ids_by_epoch.key(epoch) + return _evidence_from_ids(path.iter(_db()) if path.len(_db()) else []) + + +def verify_rocks_projection(db: RocksDb, expected: dict | None = None) -> dict: + """Verify counts, chain end, and representative direct lookups.""" + count = ROOT.events.len(db) + report = { + "event_count": count, + "chain_head": ROOT.meta.chain_head.get(db), + "evidence_count": ROOT.evidence_by_id.len(db), + "emission_count": ROOT.emissions_by_epoch.len(db), + "discovery_count": ROOT.discoveries_by_epoch.len(db), + "epoch_count": ROOT.epochs.len(db), + "representative_lookups": {}, + } + if count: + first = ROOT.events.get(db, 0) + last = ROOT.events.get(db, count - 1) + report["first_type"] = first.get("type") if first else None + report["last_type"] = last.get("type") if last else None + for label, path in ( + ("latest_emission", ROOT.latest_emission), + ("latest_discovery", ROOT.latest_discovery), + ): + row = path.get(db) + report["representative_lookups"][label] = bool(row) + evidence_page = ROOT.evidence_by_id.keys_page(db, limit=1) + if evidence_page.items: + event_id = evidence_page.items[0] + report["representative_lookups"]["evidence_event_id"] = event_id + report["representative_lookups"]["evidence_found"] = bool( + ROOT.evidence_by_id.key(event_id).get(db) + ) + + if expected: + checks = { + "event_count": report["event_count"] == expected["event_count"], + "chain_head": report["chain_head"] == expected["chain_head"], + "evidence_count": ( + report["evidence_count"] == expected["evidence_count"] + ), + "emission_count": ( + report["emission_count"] == expected["emission_count"] + ), + "discovery_count": ( + report["discovery_count"] == expected["discovery_count"] + ), + "chain_links": expected.get("chain_mismatches", 0) == 0, + } + report["checks"] = checks + report["ok"] = all(checks.values()) + else: + report["ok"] = True + return report + + +def import_jsonl_tape( + tape_path: pathlib.Path | str, + rocks_path: pathlib.Path | str, + *, + batch_size: int = 100, + force: bool = False, +) -> dict: + """One-shot streaming projection import. + + Reads one JSON object per line and commits bounded groups. On any failure the + incomplete Rocks projection is destroyed; rerun from zero. + """ + if batch_size < 1: + raise ValueError("batch_size must be at least 1") + tape_path = pathlib.Path(tape_path) + rocks_path = pathlib.Path(rocks_path) + if force and rocks_path.exists(): + RocksDb.open(rocks_path).destroy() + + db = RocksDb.open(rocks_path) + if ROOT.events.len(db): + db.close() + raise RuntimeError( + f"Rocks projection is not empty: {rocks_path}; use --force to rebuild" + ) + + digest = hashlib.sha256() + expected = { + "event_count": 0, + "evidence_count": 0, + "emission_count": 0, + "discovery_count": 0, + "chain_head": None, + "chain_mismatches": 0, + } + batch = db.batch() + pending = 0 + pending_evidence_ids: set[str] = set() + pending_emission_epochs: set[int] = set() + pending_discovery_epochs: set[int] = set() + try: + with tape_path.open("rb") as tape: + for line_number, raw_line in enumerate(tape, start=1): + digest.update(raw_line) + if not raw_line.strip(): + continue + try: + row = json.loads(raw_line) + except Exception as exc: + raise ValueError( + f"invalid JSONL at line {line_number}: {exc}" + ) from exc + if not isinstance(row, dict) or not isinstance(row.get("type"), str): + raise ValueError(f"invalid tape event at line {line_number}") + + if row["type"] == "evidence": + event_id = row.get("event_id") + if not isinstance(event_id, str): + raise ValueError( + f"evidence missing event_id at line {line_number}" + ) + if ( + event_id in pending_evidence_ids + or ROOT.evidence_by_id.contains(db, event_id) + ): + raise ValueError( + f"duplicate evidence event_id {event_id} " + f"at line {line_number}" + ) + pending_evidence_ids.add(event_id) + expected["evidence_count"] += 1 + previous = row.get("previous_event_sha256") + expected_previous = expected["chain_head"] or "0" * 64 + if previous != expected_previous: + expected["chain_mismatches"] += 1 + elif row["type"] == "emission": + epoch = int(row["epoch"]) + if ( + epoch in pending_emission_epochs + or ROOT.emissions_by_epoch.contains(db, epoch) + ): + raise ValueError( + f"duplicate emission epoch {epoch} " + f"at line {line_number}" + ) + pending_emission_epochs.add(epoch) + expected["emission_count"] += 1 + elif row["type"] == "gitdiscovery": + epoch = int(row["epoch"]) + if ( + epoch in pending_discovery_epochs + or ROOT.discoveries_by_epoch.contains(db, epoch) + ): + raise ValueError( + f"duplicate discovery epoch {epoch} " + f"at line {line_number}" + ) + pending_discovery_epochs.add(epoch) + expected["discovery_count"] += 1 + + chain = _chain_digest(row) + if chain: + expected["chain_head"] = chain + batch.extend(_event_writes(row)) + expected["event_count"] += 1 + pending += 1 + if pending >= batch_size: + batch.commit(Durability.WAL_ONLY) + batch = db.batch() + pending = 0 + pending_evidence_ids.clear() + pending_emission_epochs.clear() + pending_discovery_epochs.clear() + + if pending: + batch.commit(Durability.WAL_ONLY) + db.apply( + [ + ROOT.meta.imported_count.set(expected["event_count"]), + ROOT.meta.tape_sha256.set(digest.hexdigest()), + ], + Durability.SYNC_WAL, + ) + report = verify_rocks_projection(db, expected) + report.update({ + "tape_path": str(tape_path), + "rocks_path": str(rocks_path), + "tape_sha256": digest.hexdigest(), + "chain_mismatches": expected["chain_mismatches"], + }) + if not report["ok"]: + raise RuntimeError(f"projection verification failed: {report['checks']}") + db.close() + return report + except BaseException: + try: + db.destroy() + finally: + pass + raise async def append_evidence(epoch: int, kind: str, payload: dict) -> Evidence: - """Durably append one Evidence event under process + file locks.""" + """Atomically update canonical event, chain head, blobs, and indexes.""" # Logical identity ignores chain links / wall clock so restarts stay idempotent. event_id = _content_id("ev", { "schema_version": EVIDENCE_SCHEMA_VERSION, @@ -408,53 +855,25 @@ async def append_evidence(epoch: int, kind: str, payload: dict) -> Evidence: "payload": payload, }) async with _LEDGER_LOCK: - lock_path = _ledger_lock_path() - lock_path.parent.mkdir(parents=True, exist_ok=True) - with lock_path.open("a+b") as lock_file: - fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) - try: - events = store.read() - existing = next( - ( - e for e in events - if isinstance(e, Evidence) and e.event_id == event_id - ), - None, - ) - if existing: - return existing - recorded_at_ms = int(time.time() * 1000) - previous = _previous_event_sha256(events) - evidence = Evidence( - schema_version=EVIDENCE_SCHEMA_VERSION, - event_id=event_id, - epoch=epoch, - kind=kind, - recorded_at_ms=recorded_at_ms, - previous_event_sha256=previous, - payload=payload, - ) - - def append_once(current): - if any( - isinstance(e, Evidence) and e.event_id == event_id - for e in current - ): - return None - return evidence - - appended = await store.atomic(append_once) - result = appended or next( - e for e in store.read() - if isinstance(e, Evidence) and e.event_id == event_id - ) - try: - with open(store.path, "rb") as fh: - os.fsync(fh.fileno()) - except OSError: - pass - finally: - fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + db = _db() + existing = _typed(ROOT.evidence_by_id.key(event_id).get(db)) + if isinstance(existing, Evidence): + return existing + stored_payload, blobs = _externalize_payload(payload) + evidence = Evidence( + schema_version=EVIDENCE_SCHEMA_VERSION, + event_id=event_id, + epoch=epoch, + kind=kind, + recorded_at_ms=int(time.time() * 1000), + previous_event_sha256=( + ROOT.meta.chain_head.get(db) or "0" * 64 + ), + payload=stored_payload, + ) + row = to_dict(evidence) + db.apply(_event_writes(row, blobs=blobs), Durability.SYNC_WAL) + result = evidence await broadcast_audit( kind, f"{kind}: {payload.get('summary') or event_id[:24]}", @@ -466,24 +885,32 @@ async def append_evidence(epoch: int, kind: str, payload: dict) -> Evidence: return result -def evidence_by_kind(kind: str | None = None) -> list[Evidence]: - rows = [e for e in store.read() if isinstance(e, Evidence)] - if kind is None: - return rows - return [e for e in rows if e.kind == kind] +async def _append_typed_event(event_) -> object: + """Append a non-Evidence tape event and its indexes atomically.""" + async with _LEDGER_LOCK: + if isinstance(event_, Emission): + existing = _emission_for_epoch(event_.epoch) + if existing is not None: + return existing + if isinstance(event_, GitDiscovery): + existing = _discovery_for_epoch(event_.epoch) + if existing is not None: + return existing + _db().apply(_event_writes(to_dict(event_)), Durability.SYNC_WAL) + return event_ def find_evidence(event_id: str) -> Evidence | None: - return next( - (e for e in store.read() if isinstance(e, Evidence) and e.event_id == event_id), - None, - ) + row = _typed(ROOT.evidence_by_id.key(event_id).get(_db())) + return row if isinstance(row, Evidence) else None def find_evidence_payload(kind: str, key: str, value: str) -> Evidence | None: - for e in evidence_by_kind(kind): - if e.payload.get(key) == value: - return e + event_id = ROOT.evidence_by_entity.key((kind, key, value)).get(_db()) + if event_id: + event_ = find_evidence(event_id) + if event_ and event_.kind == kind: + return event_ return None @@ -523,31 +950,18 @@ def _blob_text(blob) -> str: def _discovery_for_epoch(epoch: int) -> GitDiscovery | None: - return next( - ( - e for e in store.read() - if isinstance(e, GitDiscovery) and e.epoch == epoch - ), - None, - ) + stored = ROOT.discoveries_by_epoch.key(epoch).get(_db()) + row = _typed(_hydrate_heavy_fields(stored) if stored else None) + return row if isinstance(row, GitDiscovery) else None def _emission_for_epoch(epoch: int) -> Emission | None: - return next( - ( - e for e in store.read() - if isinstance(e, Emission) and e.epoch == epoch - ), - None, - ) + row = _typed(ROOT.emissions_by_epoch.key(epoch).get(_db())) + return row if isinstance(row, Emission) else None def _epochs_in_ledger() -> list[int]: - epochs: set[int] = set() - for e in store.read(): - if isinstance(e, (GitDiscovery, Emission, Evidence)): - epochs.add(e.epoch) - return sorted(epochs) + return sorted(int(epoch) for epoch in ROOT.epochs.keys(_db())) def build_pairwise_prompt(side_a: dict, side_b: dict) -> str: @@ -1337,12 +1751,34 @@ def _replayed_discovery_state(events: list) -> tuple[set[str], dict[str, str]]: return seen_oids, seen_patches -def _build_discovery(epoch_n: int, boundary_ms: int, events: list) -> GitDiscovery: +def _build_discovery( + epoch_n: int, boundary_ms: int, events: list | None = None +) -> GitDiscovery: config = _normalized_discovery_config() digest = _config_digest(config) - prior_discoveries = [e for e in events if isinstance(e, GitDiscovery)] - initial = not prior_discoveries - seen_oids, seen_patches = _replayed_discovery_state(events) + if events is None: + db = _db() + initial = ROOT.latest_discovery.get(db) is None + pending_patches: dict[str, str] = {} + + def oid_seen(oid: str) -> bool: + return ROOT.seen_git_oids.contains(db, oid) + + def canonical_patch(patch_identity: str) -> str | None: + return ( + pending_patches.get(patch_identity) + or ROOT.canonical_patch_oids.key(patch_identity).get(db) + ) + else: + prior_discoveries = [e for e in events if isinstance(e, GitDiscovery)] + initial = not prior_discoveries + seen_oids, seen_patches = _replayed_discovery_state(events) + + def oid_seen(oid: str) -> bool: + return oid in seen_oids + + def canonical_patch(patch_identity: str) -> str | None: + return seen_patches.get(patch_identity) email_to_contributor = { email: contributor for contributor, emails in config["contributors"].items() @@ -1377,7 +1813,7 @@ def _build_discovery(epoch_n: int, boundary_ms: int, events: list) -> GitDiscove ).hexdigest(), }) - new_oids = sorted(set(locations) - seen_oids) + new_oids = sorted(oid for oid in locations if not oid_seen(oid)) pending = [] for qualified_oid in new_oids: source_rows = sorted({ @@ -1434,12 +1870,16 @@ def _build_discovery(epoch_n: int, boundary_ms: int, events: list) -> GitDiscove patch_identity = commit["patch_identity"] duplicate_patch = False if patch_identity: - if patch_identity in seen_patches: - canonical_patch_oid = seen_patches[patch_identity] + prior_canonical = canonical_patch(patch_identity) + if prior_canonical: + canonical_patch_oid = prior_canonical duplicate_patch = True else: canonical_patch_oid = commit["oid"] - seen_patches[patch_identity] = canonical_patch_oid + if events is None: + pending_patches[patch_identity] = canonical_patch_oid + else: + seen_patches[patch_identity] = canonical_patch_oid if initial and commit["committer_timestamp_ms"] < GENESIS_MS: reason = "before_genesis" @@ -1558,34 +1998,22 @@ async def discover_repositories(epoch_n: int, boundary_ms: int) -> GitDiscovery: async with _DISCOVERY_LOCK: lock_file = await asyncio.to_thread(_acquire_discovery_file_lock) try: - events = store.read() - existing = next( - ( - e for e in events - if isinstance(e, GitDiscovery) and e.epoch == epoch_n - ), - None, - ) + existing = _discovery_for_epoch(epoch_n) if existing: await _persist_discovery_evidence(existing) return existing candidate = await asyncio.to_thread( - _build_discovery, epoch_n, boundary_ms, events - ) - - def append_if_new(current_events): - if any( - isinstance(e, GitDiscovery) and e.epoch == epoch_n - for e in current_events - ): - return None - return candidate - - appended = await store.atomic(append_if_new) - discovery = appended or next( - e for e in store.read() - if isinstance(e, GitDiscovery) and e.epoch == epoch_n + _build_discovery, epoch_n, boundary_ms ) + async with _LEDGER_LOCK: + discovery = _discovery_for_epoch(epoch_n) + if discovery is None: + stored_row, blobs = _externalize_payload(to_dict(candidate)) + _db().apply( + _event_writes(stored_row, blobs=blobs), + Durability.SYNC_WAL, + ) + discovery = candidate await _persist_discovery_evidence(discovery) return discovery finally: @@ -1677,7 +2105,9 @@ def _rollup_contributor_scores( def _find_judgment(comparison_id: str, model_id: str) -> dict | None: - for e in evidence_by_kind("llm.judgment"): + path = ROOT.judgment_ids_by_comparison.key(comparison_id) + ids = path.iter(_db()) if path.len(_db()) else [] + for e in _evidence_from_ids(ids): p = e.payload if p.get("comparison_id") == comparison_id and p.get("model_id") == model_id: return p @@ -1685,11 +2115,13 @@ def _find_judgment(comparison_id: str, model_id: str) -> dict | None: def _find_ranking_models(ranking_run_id: str) -> list[str] | None: - for e in evidence_by_kind("ranking.started"): - if e.payload.get("ranking_run_id") == ranking_run_id: - models = e.payload.get("models") - if isinstance(models, list) and models: - return [str(m) for m in models] + event_ = find_evidence_payload( + "ranking.started", "ranking_run_id", ranking_run_id + ) + if event_: + models = event_.payload.get("models") + if isinstance(models, list) and models: + return [str(m) for m in models] return None @@ -2002,8 +2434,13 @@ async def rank_commits(commits: list[dict], *, epoch: int = -1): DECAY_RATE = 1 - (Decimal("0.5").ln() / (HALF_LIFE_YEARS * 12)).exp() -def pool_remaining(events: list) -> Decimal: - emitted = sum(Decimal(e.total_emitted) for e in events if isinstance(e, Emission)) +def pool_remaining(events: list | None = None) -> Decimal: + if events is None: + emitted = Decimal(ROOT.meta.total_emitted.get(_db()) or "0") + else: + emitted = sum( + Decimal(e.total_emitted) for e in events if isinstance(e, Emission) + ) return CONTRIBUTOR_POOL - emitted @@ -2058,10 +2495,11 @@ async def run_emission(epoch_n, boundary_ms): else: ranking, models, ranking_info = ranked - def make_emission(events): - if epoch_n in {e.epoch for e in events if isinstance(e, Emission)}: - return None - pool_now = pool_remaining(events) + async with _LEDGER_LOCK: + existing = _emission_for_epoch(epoch_n) + if existing: + return existing + pool_now = pool_remaining() emission_now = pool_now * DECAY_RATE if ranking else Decimal("0") normalized_ranking = {} distributions = {} @@ -2078,7 +2516,7 @@ async def run_emission(epoch_n, boundary_ms): distributions[contributor] = amount allocated += amount distributions[contributors[-1]] = emission_now - allocated - return Emission( + entry = Emission( epoch=epoch_n, timestamp_ms=boundary_ms, discovery_snapshot_id=discovery.snapshot_id, @@ -2093,8 +2531,7 @@ async def run_emission(epoch_n, boundary_ms): ranking_run_id=ranking_info.get("ranking_run_id", ""), ranking_event_id=ranking_info.get("ranking_event_id", ""), ) - - entry = await store.atomic(make_emission) + _db().apply(_event_writes(to_dict(entry)), Durability.SYNC_WAL) if entry: PROCESS_STATE["running"] = False await broadcast_audit( @@ -2135,7 +2572,7 @@ async def distribute_usdc(holdings, treasury_balance): treasury_balance=str(treasury_balance), distributions={w: str(treasury_balance * b / total) for w, b in holdings.items()}, ) - await store.append(entry) + await _append_typed_event(entry) return entry @@ -2146,8 +2583,7 @@ async def distribute_usdc(holdings, treasury_balance): async def epoch_loop(): while True: epoch_n, current_start, next_boundary = current_epoch() - processed = {e.epoch for e in store.read() if isinstance(e, Emission)} - if epoch_n >= 0 and epoch_n not in processed: + if epoch_n >= 0 and not ROOT.emissions_by_epoch.contains(_db(), epoch_n): try: await run_emission(epoch_n, current_start) except Exception as exc: @@ -2238,7 +2674,12 @@ async def get_ledger(offset: int = 0, limit: int = 100, full: int = 0): """List of ledger dicts. Heavy blobs stripped unless full=1.""" limit = max(1, min(limit, 500)) rows = [] - for e in store.read()[offset:offset + limit]: + db = _db() + total = ROOT.events.len(db) + for index in range(max(0, offset), min(total, max(0, offset) + limit)): + e = _typed(ROOT.events.get(db, index)) + if e is None: + continue if isinstance(e, Evidence) and not full: rows.append(_evidence_summary(e)) else: @@ -2249,33 +2690,33 @@ async def get_ledger(offset: int = 0, limit: int = 100, full: int = 0): @app.middleware("http") async def ledger_ready_gate(request: Request, call_next): - if request.url.path == "/api/health": - return await call_next(request) - if isinstance(store, DeferredJsonlStore) and not store.ready: - return JSONResponse( - { - "ok": False, - "loading": True, - "message": "replaying ledger into memory", - }, - status_code=503, - ) return await call_next(request) @app.get("/api/health") async def get_health(): - """Liveness only — must not touch the ledger (boot + ranking).""" + """Report readiness using only the exact list-length metadata key.""" + error = STATE_STATUS["error"] + event_count = STATE_STATUS["event_count"] + if STATE_STATUS["ready"] and state_db is not None: + try: + event_count = ROOT.events.len(state_db) + except Exception as exc: + error = str(exc) + ready = bool(STATE_STATUS["ready"] and not error) return { - "ok": True, - "ledger_ready": bool(getattr(store, "ready", True)), + "ok": ready, + "ledger_ready": ready, + "importing": bool(STATE_STATUS["importing"]), + "error": error, + "event_count": event_count, } @app.get("/api/epoch") async def get_epoch(): epoch_n, start, next_b = current_epoch() - pool = pool_remaining(store.read()) + pool = pool_remaining() return { "epoch": epoch_n, "start_ms": start, "next_boundary_ms": next_b, "total_supply": str(TOTAL_SUPPLY), @@ -2291,18 +2732,16 @@ async def get_epoch(): @app.get("/api/ranking") async def get_ranking(): - emissions = [e for e in store.read() if isinstance(e, Emission)] - if not emissions: + latest = _typed(ROOT.latest_emission.get(_db())) + if not isinstance(latest, Emission): return {"ranking": {}, "epoch": -1} - latest = emissions[-1] return {"ranking": latest.ranking, "epoch": latest.epoch} @app.get("/api/status") async def get_status(): - events = store.read() - discoveries = [e for e in events if isinstance(e, GitDiscovery)] - emissions = [e for e in events if isinstance(e, Emission)] + latest_discovery = _typed(ROOT.latest_discovery.get(_db())) + latest_emission = _typed(ROOT.latest_emission.get(_db())) return { **PROCESS_STATE, "epoch": current_epoch()[0], @@ -2310,29 +2749,32 @@ async def get_status(): "sse_clients": len(SSE_CLIENTS), "latest_discovery": ( { - "epoch": discoveries[-1].epoch, - "snapshot_id": discoveries[-1].snapshot_id, - "observations": len(discoveries[-1].observations), - "eligible_commits": len(discoveries[-1].commits), + "epoch": latest_discovery.epoch, + "snapshot_id": latest_discovery.snapshot_id, + "observations": len(latest_discovery.observations), + "eligible_commits": len(latest_discovery.commits), } - if discoveries else None + if isinstance(latest_discovery, GitDiscovery) else None ), "latest_emission": ( { - "epoch": emissions[-1].epoch, - "total_emitted": emissions[-1].total_emitted, - "ranking": emissions[-1].ranking, + "epoch": latest_emission.epoch, + "total_emitted": latest_emission.total_emitted, + "ranking": latest_emission.ranking, } - if emissions else None + if isinstance(latest_emission, Emission) else None ), } @app.get("/api/contributor/{github_username}") async def get_contributor(github_username: str): + emissions = [ + _typed(row) for _, row in ROOT.emissions_by_epoch.iter(_db()) + ] history = [ {"epoch": e.epoch, "amount": e.distributions[github_username], "rank_score": e.ranking.get(github_username)} - for e in store.read() + for e in emissions if isinstance(e, Emission) and github_username in e.distributions ] return {"contributor": github_username, "total_earned": str(sum(Decimal(h["amount"]) for h in history)), "history": history} @@ -2381,7 +2823,7 @@ async def test_emit(): """Integration tests only: run the next unprocessed emission.""" if os.environ.get("ALLOW_TEST_TRIGGERS") != "1": return Response(status_code=404) - processed = {e.epoch for e in store.read() if isinstance(e, Emission)} + processed = set(int(epoch) for epoch in ROOT.emission_epochs.keys(_db())) n = 0 while n in processed: n += 1 @@ -2461,7 +2903,14 @@ def _side_label(side: dict) -> str: def _judgments_by_comparison( judgments: list[Evidence] | None = None, ) -> dict[str, list[Evidence]]: - rows = judgments if judgments is not None else evidence_by_kind("llm.judgment") + if judgments is None: + out: dict[str, list[Evidence]] = {} + for comparison_id in ROOT.judgment_ids_by_comparison.keys(_db()): + out[str(comparison_id)] = _judgments_for_comparison( + str(comparison_id) + ) + return out + rows = judgments out: dict[str, list[Evidence]] = {} for e in rows: cid = e.payload.get("comparison_id") @@ -2471,7 +2920,9 @@ def _judgments_by_comparison( def _judgments_for_comparison(comparison_id: str) -> list[Evidence]: - return _judgments_by_comparison().get(comparison_id, []) + path = ROOT.judgment_ids_by_comparison.key(comparison_id) + ids = path.iter(_db()) if path.len(_db()) else [] + return _evidence_from_ids(ids) def _judgment_blocks(judgments: list[Evidence]) -> list: @@ -2549,14 +3000,11 @@ async def epochs_index(): rows = [] for epoch in epochs: emission = _emission_for_epoch(epoch) - evidence_n = sum(1 for e in evidence_by_kind() if e.epoch == epoch) - disc = next( - ( - e for e in evidence_by_kind("git.discovery_completed") - if e.epoch == epoch - ), - None, + evidence_n = int( + ROOT.evidence_count_by_epoch.key(epoch).get(_db()) ) + discovery_event_id = ROOT.discovery_evidence_by_epoch.key(epoch).get(_db()) + disc = find_evidence(discovery_event_id) if discovery_event_id else None detail = [] if disc: detail.append( @@ -2584,7 +3032,7 @@ async def epochs_index(): @app.get("/epochs/{epoch}") async def epoch_detail(epoch: int): emission = _emission_for_epoch(epoch) - evidence_rows = [e for e in evidence_by_kind() if e.epoch == epoch] + evidence_rows = _evidence_for_epoch(epoch) if not evidence_rows and emission is None: return _evidence_page(f"epoch {epoch}", [ _evidence_nav(), @@ -2807,10 +3255,16 @@ async def commit_detail(commit_id: str): ]) p = ev.payload epoch = ev.epoch - judgments_by_cmp = _judgments_by_comparison() related_cmp = [] - for cmp in evidence_by_kind("comparison.input"): - if cmp.epoch != epoch: + comparison_path = ROOT.comparison_ids_by_commit.key(commit_id) + comparison_ids = ( + comparison_path.iter(_db()) if comparison_path.len(_db()) else [] + ) + for comparison_id in comparison_ids: + cmp = find_evidence_payload( + "comparison.input", "comparison_id", comparison_id + ) + if not cmp: continue sa, sb = cmp.payload.get("side_a") or {}, cmp.payload.get("side_b") or {} ids = { @@ -2821,7 +3275,7 @@ async def commit_detail(commit_id: str): } if commit_id not in ids: continue - cid = cmp.payload.get("comparison_id") + cid = comparison_id if not cid: continue related_cmp.append(["article.dense-card", @@ -2832,7 +3286,7 @@ async def commit_detail(commit_id: str): " vs ", _side_label(sb), ], - *_judgment_blocks(judgments_by_cmp.get(cid, [])), + *_judgment_blocks(_judgments_for_comparison(cid)), ]) return _evidence_page(f"commit {commit_id[:24]}", [ _evidence_nav(_a(_evidence_path("epoch", str(epoch)), f"epoch {epoch}")), @@ -2885,14 +3339,19 @@ async def comparison_detail(comparison_id: str): side_b = p.get("side_b") or {} judgments = _judgments_for_comparison(comparison_id) attempt_links = [] - for a in evidence_by_kind("llm.attempt_started"): - if a.payload.get("comparison_id") == comparison_id: - aid = a.payload.get("attempt_id") - if aid: - attempt_links.append(( - f"{a.payload.get('model_id')} #{a.payload.get('attempt_number')}", - _evidence_path("attempt", aid), - )) + attempt_path = ROOT.attempt_ids_by_comparison.key(comparison_id) + attempt_event_ids = ( + attempt_path.iter(_db()) if attempt_path.len(_db()) else [] + ) + for a in _evidence_from_ids(attempt_event_ids): + if a.kind != "llm.attempt_started": + continue + aid = a.payload.get("attempt_id") + if aid: + attempt_links.append(( + f"{a.payload.get('model_id')} #{a.payload.get('attempt_number')}", + _evidence_path("attempt", aid), + )) judgment_links = [ ( j.payload.get("summary") or j.payload.get("judgment_id", j.event_id), @@ -2977,11 +3436,8 @@ async def attempt_detail(attempt_id: str): base = started or finished assert base is not None p = {**(started.payload if started else {}), **(finished.payload if finished else {})} - judgment = None - for j in evidence_by_kind("llm.judgment"): - if j.payload.get("attempt_id") == attempt_id: - judgment = j - break + judgment_event_id = ROOT.judgment_id_by_attempt.key(attempt_id).get(_db()) + judgment = find_evidence(judgment_event_id) if judgment_event_id else None comparison_id = p.get("comparison_id") return _evidence_page(f"attempt {attempt_id[:24]}", [ _evidence_nav( @@ -3441,9 +3897,14 @@ table.dense td.msg { color: var(--prose); font-family: var(--font-prose); } def _watch_initial_state() -> dict: - events = store.read() + db = _db() + total = ROOT.events.len(db) + events = [ + _typed(ROOT.events.get(db, index)) + for index in range(max(0, total - 40), total) + ] feed = [] - for event_ in events[-40:]: + for event_ in events: if isinstance(event_, GitDiscovery): feed.append({ "id": f"discovery-{event_.snapshot_id}", @@ -3657,7 +4118,7 @@ async def watch(): async def redeem(github_user: str, wallet_address: str): - await store.append(Redemption( + await _append_typed_event(Redemption( timestamp_ms=int(time.time() * 1000), github_user=github_user, wallet_address=wallet_address, @@ -3683,11 +4144,18 @@ async def index(request: Request): ], ]) - events = store.read() - history = [e for e in events if isinstance(e, Emission) and user in e.distributions] + emissions = [ + _typed(row) for _, row in ROOT.emissions_by_epoch.iter(_db()) + ] + history = [ + e for e in emissions + if isinstance(e, Emission) and user in e.distributions + ] total_earned = sum(Decimal(e.distributions[user]) for e in history) - emissions = [e for e in events if isinstance(e, Emission)] - latest_rank = emissions[-1].ranking.get(user) if emissions else None + latest = _typed(ROOT.latest_emission.get(_db())) + latest_rank = ( + latest.ranking.get(user) if isinstance(latest, Emission) else None + ) redeem_form = ["form", {"method": "post"}, *signer.snippet_hidden(f"redeem({json.dumps(user)}, $wallet_address)"), @@ -3758,17 +4226,10 @@ async def callback(request: Request, code: str): @app.on_event("startup") async def startup(): - async def boot(): - if isinstance(store, DeferredJsonlStore) and not store.ready: - print("replaying ledger…", flush=True) - await asyncio.to_thread(store.load_sync) - n = len(store.read()) - print(f"ledger ready: {n} events", flush=True) - if os.environ.get("DISABLE_EPOCH_LOOP") != "1": - asyncio.create_task(epoch_loop()) - - # Do not block bind/health on ledger replay. - asyncio.create_task(boot()) + status = await asyncio.to_thread(prepare_state_sync) + print(f"rocks ledger ready: {status['event_count']} events", flush=True) + if os.environ.get("DISABLE_EPOCH_LOOP") != "1": + asyncio.create_task(epoch_loop()) if __name__ == "__main__": diff --git a/fly.toml b/fly.toml index 4178bed71f506320c3481f4a6919a0cf8d798711..928e890a374e37a8f061018fd377d425586ddea2 100644 --- a/fly.toml +++ b/fly.toml @@ -7,6 +7,7 @@ primary_region = "ord" [env] GENESIS_MS = "1775364391260" JSONL_PATH = "/data/ledger.jsonl" + ROCKS_PATH = "/data/constitution.rocks" GIT_MIRROR_DIR = "/data/git" PORT = "8080" diff --git a/pyproject.toml b/pyproject.toml index 65092f570614a9a5473fea04150a9871d5e0490c..54e55b598c7a53da3f32fa5802cca3fa1db3fcb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,8 @@ name = "slug-constitution" version = "0.1.0" requires-python = ">=3.11" dependencies = [ - "evaleval==0.2.7", + "evaleval @ git+https://github.com/tommy-mor/evaleval.git@584225b43f37261b446ad04169aaddf77ca6c201", + "rocksdict>=0.3.29", "numpy", "httpx", "fastapi", diff --git a/scripts/import-ledger.py b/scripts/import-ledger.py new file mode 100755 index 0000000000000000000000000000000000000000..f8c8e44d9c7ba6e743815e64028926349fded5d8 --- /dev/null +++ b/scripts/import-ledger.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Project archival JSONL into a fresh RocksDB database.""" + +from __future__ import annotations + +import argparse +import json +import os +import pathlib +import sys + +os.environ.setdefault("SESSION_SECRET", "offline-migration") +os.environ.setdefault("GENESIS_MS", "0") + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from constitution import import_jsonl_tape # noqa: E402 + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Stream ledger.jsonl into a fresh Rocks projection. " + "An incomplete projection is deleted on failure." + ) + ) + parser.add_argument("tape", type=pathlib.Path) + parser.add_argument("rocks", type=pathlib.Path) + parser.add_argument("--batch-size", type=int, default=100) + parser.add_argument( + "--force", + action="store_true", + help="destroy an existing projection and rebuild from zero", + ) + args = parser.parse_args() + report = import_jsonl_tape( + args.tape, + args.rocks, + batch_size=args.batch_size, + force=args.force, + ) + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/integration.clj b/tests/integration.clj index 39b2476cb5ddf80733769f61343c81ba7287a974..7f58dced09efddacb67eb311196a6a004fb65dc2 100644 --- a/tests/integration.clj +++ b/tests/integration.clj @@ -312,7 +312,7 @@ "SLUG_SOCIAL_BASE_URL" slug-live-base "SLUG_MODEL_RANK_PARENT" slug-model-parent "OPENROUTER_API_KEY" ""})) - (bind py @(p/process ["uv" "run" "python" "-c" + (bind py @(p/process [(str root "/.venv/bin/python") "-c" (str "import asyncio, json, os\n" "os.environ.setdefault('SESSION_SECRET','x')\n" "os.environ.setdefault('GENESIS_MS','1')\n" @@ -372,6 +372,7 @@ (letlocals (bind tmp-dir (str (fs/create-temp-dir {:prefix "constitution-test-"}))) (bind jsonl-path (str tmp-dir "/ledger.jsonl")) + (bind rocks-path (str tmp-dir "/ledger.rocks")) (bind server-port (pick-port)) (bind or-port (pick-port)) ; openrouter mock (bind base-url (str "http://127.0.0.1:" server-port)) @@ -380,6 +381,7 @@ (bind genesis-ms (str (- (System/currentTimeMillis) (* 35 24 60 60 1000)))) (bind root (project-root)) + (bind python-bin (str root "/.venv/bin/python")) (test-live-slug-model-council! root) (bind repo-a (make-git-repository! tmp-dir "repo-a" "alice" "alice@example.test" "alice.txt")) @@ -396,6 +398,7 @@ {"SESSION_SECRET" "test-secret" "GENESIS_MS" genesis-ms "JSONL_PATH" jsonl-path + "ROCKS_PATH" rocks-path "GIT_MIRROR_DIR" (str tmp-dir "/mirrors") "REPOSITORIES_JSON" repositories-json "CONTRIBUTORS_JSON" contributors-json @@ -427,10 +430,14 @@ ;; 2. seed ledger so pool_remaining has history to read (seed-ledger jsonl-path) (assert! (fs/exists? jsonl-path) "ledger.jsonl seeded") + (command! [python-bin (str root "/scripts/import-ledger.py") + jsonl-path rocks-path] + {:dir root :env server-env}) + (assert! (fs/exists? rocks-path) "Rocks projection imported") ;; 3. start constitution server (println (str "\nstarting server on :" server-port " (data: " tmp-dir ")")) - (bind server (p/process ["uv" "run" "constitution.py"] + (bind server (p/process [python-bin "constitution.py"] {:out :inherit :err :inherit :env server-env :dir root})) (reset! !server server) @@ -566,8 +573,8 @@ (deref server) (reset! !server nil) - (println "restarting server from same JSONL…") - (bind server2 (p/process ["uv" "run" "constitution.py"] + (println "restarting server from same Rocks projection…") + (bind server2 (p/process [python-bin "constitution.py"] {:out :inherit :err :inherit :env server-env :dir root})) (reset! !server2 server2) diff --git a/tests/test_evidence.py b/tests/test_evidence.py index 708962fdd82839f48f135b509136b4e92acfaf6b..fcb775dfbf0f672b0c7dbb7e17cf921f021c88f1 100644 --- a/tests/test_evidence.py +++ b/tests/test_evidence.py @@ -14,9 +14,11 @@ import constitution as c @pytest.fixture def evidence_store(tmp_path, monkeypatch): - monkeypatch.setattr(c, "store", c.JsonlStore(tmp_path / "ledger.jsonl")) + db = c.RocksDb.open(tmp_path / "ledger.rocks") + monkeypatch.setattr(c, "state_db", db) monkeypatch.setattr(c, "PUBLIC_BASE_URL", "http://test.local") - return tmp_path + yield tmp_path + db.close() def test_bytes_blob_roundtrip_crlf_nul_invalid_utf8(): @@ -41,7 +43,8 @@ def test_append_evidence_is_idempotent_and_hash_chained(evidence_store): "summary": "done", "snapshot_id": "abc", })) assert second.previous_event_sha256 == first.event_id.split("_", 1)[-1] - events = [e for e in c.store.read() if isinstance(e, c.Evidence)] + ids = c.ROOT.event_ids_by_epoch.key(1).iter(c._db()) + events = c._evidence_from_ids(ids) assert len(events) == 2 @@ -234,7 +237,7 @@ def test_ranking_resume_skips_duplicate_provider_calls(evidence_store, monkeypat def test_epochs_index_lists_epochs(evidence_store): - asyncio.run(c.store.append(c.Emission( + asyncio.run(c._append_typed_event(c.Emission( epoch=3, timestamp_ms=1, pool_before="1", diff --git a/tests/test_git_discovery.py b/tests/test_git_discovery.py index c9c1b4d16f64c4433e2588e94caf373ef44d9c72..0637fca60567fb234ccf31106952ccc733e818d2 100644 --- a/tests/test_git_discovery.py +++ b/tests/test_git_discovery.py @@ -90,13 +90,16 @@ def push(work: Path, *refs: str) -> None: @pytest.fixture def discovery_config(tmp_path, monkeypatch): + db = c.RocksDb.open(tmp_path / "ledger.rocks") + monkeypatch.setattr(c, "state_db", db) monkeypatch.setattr(c, "GIT_MIRROR_DIR", tmp_path / "mirrors") monkeypatch.setattr(c, "GENESIS_MS", 1_000_000) monkeypatch.setattr(c, "CONTRIBUTORS", { "alice": ["author@example.test"], "bob": ["bob@example.test"], }) - return tmp_path + yield tmp_path + db.close() def configure(monkeypatch, repositories): @@ -370,9 +373,6 @@ def test_concurrent_same_epoch_discovery_appends_once( configure(monkeypatch, [{ "id": "one", "url": str(remote), "refs": ["refs/heads/main"], }]) - ledger_path = discovery_config / "ledger.jsonl" - monkeypatch.setattr(c, "store", c.JsonlStore(ledger_path)) - async def run_both(): return await asyncio.gather( c.discover_repositories(0, c.GENESIS_MS), @@ -382,7 +382,7 @@ def test_concurrent_same_epoch_discovery_appends_once( left, right = asyncio.run(run_both()) assert left.snapshot_id == right.snapshot_id discoveries = [ - event for event in c.store.read() if isinstance(event, c.GitDiscovery) + c._typed(row) for _, row in c.ROOT.discoveries_by_epoch.iter(c._db()) ] assert len(discoveries) == 1 @@ -390,8 +390,6 @@ def test_concurrent_same_epoch_discovery_appends_once( def test_empty_epoch_records_zero_emission_without_burning_pool( discovery_config, monkeypatch ): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) - async def discover(_epoch, _boundary): return SimpleNamespace( observations=[], commits=[], snapshot_id="empty-snapshot" @@ -411,8 +409,6 @@ def test_empty_epoch_records_zero_emission_without_burning_pool( def test_emission_distribution_sums_exactly_to_total( discovery_config, monkeypatch ): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) - async def discover(_epoch, _boundary): return SimpleNamespace( observations=[{"x": 1}], @@ -437,8 +433,6 @@ def test_emission_distribution_sums_exactly_to_total( def test_single_commit_ranking_skips_pairwise( discovery_config, monkeypatch, ): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) - async def models(n=3): return [] @@ -457,7 +451,6 @@ def test_single_commit_ranking_skips_pairwise( def test_same_contributor_multiple_commits_runs_pairwise( discovery_config, monkeypatch, ): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) monkeypatch.setattr(c, "PREFERRED_COUNCIL_MODELS", []) calls = {"n": 0} @@ -505,17 +498,13 @@ def test_same_contributor_multiple_commits_runs_pairwise( assert ranking["alice"] > 0 assert used == ["m1", "m2", "m3"] assert calls["n"] >= 3 - completed = next( - e for e in c.store.read() - if isinstance(e, c.Evidence) and e.kind == "ranking.completed" - ) + completed = c._evidence_for_kind("ranking.completed")[0] assert len(completed.payload["commit_ranking"]) == 3 assert "alice" in completed.payload["contributor_ranking"] assert info["ranking_event_id"] def test_all_council_failures_abort_ranking(discovery_config, monkeypatch): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) monkeypatch.setattr(c, "PREFERRED_COUNCIL_MODELS", []) async def models(n=3): @@ -541,7 +530,6 @@ def test_all_council_failures_abort_ranking(discovery_config, monkeypatch): def test_one_council_failure_retires_model_and_continues(discovery_config, monkeypatch): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) monkeypatch.setattr(c, "PREFERRED_COUNCIL_MODELS", []) async def models(n=3): @@ -586,16 +574,10 @@ def test_one_council_failure_retires_model_and_continues(discovery_config, monke ranking, used, _info = asyncio.run(c.rank_commits(commits, epoch=0)) assert set(ranking) == {"alice", "bob"} assert used == ["solid"] - retired = [ - e for e in c.store.read() - if isinstance(e, c.Evidence) and e.kind == "llm.council_member_failed" - ] + retired = c._evidence_for_kind("llm.council_member_failed") assert len(retired) == 1 assert retired[0].payload["model_id"] == "broken" - completed = next( - e for e in c.store.read() - if isinstance(e, c.Evidence) and e.kind == "ranking.completed" - ) + completed = c._evidence_for_kind("ranking.completed")[0] assert completed.payload["models_failed"] == ["broken"] @@ -617,7 +599,6 @@ def test_multi_commit_ranking_requires_openrouter_key(monkeypatch): def test_watch_page_has_live_controls_progress_and_key_warning( discovery_config, monkeypatch ): - monkeypatch.setattr(c, "store", c.JsonlStore(discovery_config / "ledger.jsonl")) monkeypatch.setattr(c, "OPENROUTER_API_KEY", "") monkeypatch.setattr(c, "current_epoch", lambda: (3, 0, 1)) response = asyncio.run(c.watch()) diff --git a/tests/test_state_migration.py b/tests/test_state_migration.py new file mode 100644 index 0000000000000000000000000000000000000000..18a6f11b766a3ed759ad9793d83e341fd783ac53 --- /dev/null +++ b/tests/test_state_migration.py @@ -0,0 +1,345 @@ +"""Streaming JSONL projection and direct Rocks path equivalence.""" + +from __future__ import annotations + +import asyncio +import json + +import pytest + +import constitution as c + + +def _sample_tape(path): + emission = c.Emission( + epoch=0, + timestamp_ms=100, + pool_before=str(c.CONTRIBUTOR_POOL), + total_emitted="1", + pool_after=str(c.CONTRIBUTOR_POOL - c.Decimal("1")), + decay_rate="0.1", + distributions={"alice": "1"}, + ranking={"alice": "1"}, + models_used=["model/a"], + discovery_snapshot_id="snap-0", + evidence_schema_version=c.EVIDENCE_SCHEMA_VERSION, + ranking_run_id="rank-0", + ranking_event_id="rank-event-0", + ) + emission_row = c.to_dict(emission) + chain = c._chain_digest(emission_row) + + patch = b"@@ -1 +1 @@\n-old\n+new\n" + commit_id = "c_imported" + commit = c.Evidence( + schema_version=2, + event_id="ev_commit", + epoch=1, + kind="git.commit", + recorded_at_ms=101, + previous_event_sha256=chain, + payload={ + "commit_id": commit_id, + "oid": "sha1:" + "a" * 40, + "contributor": "alice", + "message": c._bytes_blob("imported commit"), + "patch": c._bytes_blob(patch), + "summary": "legacy embedded patch", + }, + ) + commit_row = c.to_dict(commit) + chain = c._chain_digest(commit_row) + + comparison = c.Evidence( + schema_version=2, + event_id="ev_comparison", + epoch=1, + kind="comparison.input", + recorded_at_ms=102, + previous_event_sha256=chain, + payload={ + "comparison_id": "cmp_imported", + "side_a": {"commit_id": commit_id, "contributor": "alice"}, + "side_b": {"commit_id": "c_other", "contributor": "bob"}, + "prompt": c._bytes_blob("which is better?"), + "summary": "imported comparison", + }, + ) + comparison_row = c.to_dict(comparison) + chain = c._chain_digest(comparison_row) + + judgment = c.Evidence( + schema_version=2, + event_id="ev_judgment", + epoch=1, + kind="llm.judgment", + recorded_at_ms=103, + previous_event_sha256=chain, + payload={ + "judgment_id": "jud_imported", + "comparison_id": "cmp_imported", + "attempt_id": "att_imported", + "model_id": "model/a", + "winner": "A", + "ratio": "2:1", + "explanation": "A was better.", + "summary": "imported judgment", + }, + ) + rows = [emission_row, commit_row, comparison_row, c.to_dict(judgment)] + with path.open("w") as fh: + for row in rows: + fh.write(json.dumps(row, separators=(",", ":")) + "\n") + return rows, patch + + +def test_streaming_import_preserves_contract_and_direct_indexes(tmp_path, monkeypatch): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "ledger.rocks" + rows, patch = _sample_tape(tape) + + report = c.import_jsonl_tape(tape, rocks, batch_size=2) + assert report["ok"] is True + assert report["event_count"] == len(rows) + assert report["evidence_count"] == 3 + assert report["emission_count"] == 1 + assert report["chain_mismatches"] == 0 + + db = c.RocksDb.open(rocks) + monkeypatch.setattr(c, "state_db", db) + try: + assert c.ROOT.events.len(db) == len(rows) + assert c.ROOT.events.get(db, 1) == rows[1] + assert c.find_evidence("ev_commit").payload["commit_id"] == "c_imported" + assert ( + c.find_evidence_payload( + "comparison.input", "comparison_id", "cmp_imported" + ).event_id + == "ev_comparison" + ) + judgments = c._judgments_for_comparison("cmp_imported") + assert [row.event_id for row in judgments] == ["ev_judgment"] + assert c._emission_for_epoch(0).ranking == {"alice": "1"} + + # Imported legacy envelope is byte-for-byte equivalent and remains + # transparently downloadable without conversion to a blob reference. + imported = c.find_evidence("ev_commit") + assert imported.payload["patch"] == rows[1]["payload"]["patch"] + response = asyncio.run(c.commit_patch_download("c_imported")) + assert response.body == patch + + # Runtime selectors do not consult the archival tape. + tape.unlink() + assert asyncio.run(c.get_ranking()) == { + "ranking": {"alice": "1"}, + "epoch": 0, + } + page = asyncio.run(c.get_ledger(offset=1, limit=2, full=1)) + assert [row["event_id"] for row in page] == [ + "ev_commit", + "ev_comparison", + ] + finally: + db.close() + + +def test_new_heavy_bytes_are_stored_once_and_routes_resolve_refs( + tmp_path, monkeypatch +): + db = c.RocksDb.open(tmp_path / "live.rocks") + monkeypatch.setattr(c, "state_db", db) + try: + patch = b"large patch bytes\n" * 100 + logical_payload = { + "commit_id": "c_new", + "oid": "sha1:" + "b" * 40, + "message": c._bytes_blob("new commit"), + "patch": c._bytes_blob(patch), + "summary": "new ref-backed patch", + } + expected_event_id = c._content_id("ev", { + "schema_version": c.EVIDENCE_SCHEMA_VERSION, + "epoch": 2, + "kind": "git.commit", + "payload": logical_payload, + }) + event = asyncio.run( + c.append_evidence(2, "git.commit", logical_payload) + ) + assert event.event_id == expected_event_id + ref = event.payload["patch"] + assert ref == { + "encoding": "blob", + "sha256": c._sha256_hex(patch), + "byte_length": len(patch), + } + assert c.ROOT.blobs.key(ref["sha256"]).get(db) == patch + assert asyncio.run(c.commit_patch_download("c_new")).body == patch + + # Idempotency does not append or duplicate the canonical blob. + again = asyncio.run( + c.append_evidence(2, "git.commit", logical_payload) + ) + assert again.event_id == event.event_id + assert c.ROOT.events.len(db) == 1 + assert c.ROOT.blobs.len(db) == 1 + finally: + db.close() + + +def test_failed_import_destroys_partial_projection(tmp_path): + tape = tmp_path / "bad.jsonl" + rocks = tmp_path / "bad.rocks" + tape.write_text( + json.dumps({"type": "redemption", "timestamp_ms": 1}) + "\n" + + "{not-json}\n" + ) + with pytest.raises(ValueError, match="line 2"): + c.import_jsonl_tape(tape, rocks, batch_size=1) + + db = c.RocksDb.open(rocks) + try: + assert c.ROOT.events.len(db) == 0 + finally: + db.destroy() + + +def test_import_refuses_nonempty_projection_without_force(tmp_path): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "ledger.rocks" + _sample_tape(tape) + c.import_jsonl_tape(tape, rocks) + with pytest.raises(RuntimeError, match="not empty"): + c.import_jsonl_tape(tape, rocks) + + report = c.import_jsonl_tape(tape, rocks, force=True) + assert report["event_count"] == 4 + + +def _configure_first_boot(monkeypatch, tape, rocks): + monkeypatch.setattr(c, "JSONL_PATH", tape) + monkeypatch.setattr(c, "ROCKS_PATH", rocks) + monkeypatch.setattr(c, "state_db", None) + monkeypatch.setenv("IMPORT_JSONL_ON_EMPTY", "1") + c.STATE_STATUS.update( + ready=False, importing=False, error=None, event_count=0 + ) + + +def test_first_boot_imports_before_opening_live_state(tmp_path, monkeypatch): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "constitution.rocks" + rows, _ = _sample_tape(tape) + _configure_first_boot(monkeypatch, tape, rocks) + + original_import = c.import_jsonl_tape + observed = {} + + def import_while_closed(*args, **kwargs): + observed["state_db_closed"] = c.state_db is None + observed["importing"] = c.STATE_STATUS["importing"] + return original_import(*args, **kwargs) + + monkeypatch.setattr(c, "import_jsonl_tape", import_while_closed) + status = c.prepare_state_sync() + try: + assert observed == {"state_db_closed": True, "importing": True} + assert status == { + "ready": True, + "importing": False, + "error": None, + "event_count": len(rows), + } + assert c.ROOT.events.len(c.state_db) == len(rows) + assert asyncio.run(c.get_health())["ok"] is True + finally: + c.state_db.close() + c.state_db = None + + +def test_first_boot_never_overwrites_nonempty_rocks(tmp_path, monkeypatch): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "constitution.rocks" + rows, _ = _sample_tape(tape) + c.import_jsonl_tape(tape, rocks) + tape.write_text("{this would fail if imported}\n") + _configure_first_boot(monkeypatch, tape, rocks) + + status = c.prepare_state_sync() + try: + assert status["event_count"] == len(rows) + assert c.ROOT.events.get(c.state_db, 1)["event_id"] == "ev_commit" + finally: + c.state_db.close() + c.state_db = None + + +def test_startup_opens_existing_rocks_and_reports_ready(tmp_path, monkeypatch): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "constitution.rocks" + rows, _ = _sample_tape(tape) + c.import_jsonl_tape(tape, rocks) + _configure_first_boot(monkeypatch, tape, rocks) + monkeypatch.setenv("DISABLE_EPOCH_LOOP", "1") + + asyncio.run(c.startup()) + try: + health = asyncio.run(c.get_health()) + assert health == { + "ok": True, + "ledger_ready": True, + "importing": False, + "error": None, + "event_count": len(rows), + } + finally: + c.state_db.close() + c.state_db = None + + +def test_typed_emission_retry_does_not_duplicate_canonical_row( + tmp_path, monkeypatch +): + db = c.RocksDb.open(tmp_path / "live.rocks") + monkeypatch.setattr(c, "state_db", db) + emission = c.Emission( + epoch=7, + timestamp_ms=1, + pool_before="10", + total_emitted="1", + pool_after=str(c.CONTRIBUTOR_POOL - c.Decimal("1")), + decay_rate="0.1", + distributions={}, + ranking={}, + models_used=[], + discovery_snapshot_id="snap", + evidence_schema_version=2, + ranking_run_id="rank", + ranking_event_id="event", + ) + try: + first = asyncio.run(c._append_typed_event(emission)) + second = asyncio.run(c._append_typed_event(emission)) + assert first == second + assert c.ROOT.events.len(db) == 1 + assert c.ROOT.emissions_by_epoch.len(db) == 1 + finally: + db.close() + + +@pytest.mark.parametrize("duplicate_type", ["evidence", "emission"]) +def test_import_rejects_duplicate_unique_events(tmp_path, duplicate_type): + tape = tmp_path / "ledger.jsonl" + rocks = tmp_path / "constitution.rocks" + rows, _ = _sample_tape(tape) + duplicate = rows[1] if duplicate_type == "evidence" else rows[0] + with tape.open("a") as fh: + fh.write(json.dumps(duplicate) + "\n") + + with pytest.raises(ValueError, match=f"duplicate {duplicate_type}"): + c.import_jsonl_tape(tape, rocks, batch_size=100) + db = c.RocksDb.open(rocks) + try: + assert c.ROOT.events.len(db) == 0 + finally: + db.destroy() diff --git a/uv.lock b/uv.lock index bab8e115a73870fd236bd46a1bf773b44fb5d3ee..1873bd42292f8d7e6e25df65246ab43608768676 100644 --- a/uv.lock +++ b/uv.lock @@ -46,6 +46,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, ] +[[package]] +name = "cbor2" +version = "6.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/6f/07b4af8da8bd27f640362b1ac8271d80895407f2ede0c2bcc9433c06e1ca/cbor2-6.1.3.tar.gz", hash = "sha256:8d70680acb55c04ea5b5ad86da094f9612b53d5a8a65d0f5b3aafc3ce917ecbb", size = 89503, upload-time = "2026-07-04T10:36:48.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/2e/013b2c478c41585bf5c8f9659328412eda4fe8ed30ffeb8e4fde87f8b9a3/cbor2-6.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84edab2df31c981d258d652a82a3e30eb7368d86d9d7284216282f65403a1e00", size = 421187, upload-time = "2026-07-04T10:35:54.334Z" }, + { url = "https://files.pythonhosted.org/packages/96/78/840809f265a4537fde0ba646d92d61c500435fd811961d70776fc021b7ab/cbor2-6.1.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:932e0894476fad36b186c0da6e9b1433358bea564a60ae4799e51182568ff29f", size = 463784, upload-time = "2026-07-04T10:35:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c5/4dad6125eea17b35ca5580a4f7308226c8a4511dfb91b94c329b167b6218/cbor2-6.1.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5f14159423a984c387982901f67313d6582251b6733c23e8bd925d73173691bf", size = 472119, upload-time = "2026-07-04T10:35:57.122Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f7/5f0387ee7b5601c6af5277051612b8163f82ccbddbae807bbc1326754e3e/cbor2-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:622ec874664b4db54bc6df40f82832ad30fa5c875ad85cde84392ac62bb33d15", size = 528659, upload-time = "2026-07-04T10:35:58.496Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7f/371ce0c200955a8a999e0a34398834ba88ef2fecda8437c3264c0673aa33/cbor2-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c9144756fa7c9298d5882d1f7ad379c4a0059803a4c70329965a000bc79bf02d", size = 538983, upload-time = "2026-07-04T10:35:59.828Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/3cb4d40ce9bbfb41098656a0be5a8d01f24fa54cdc6d2665cbbaefbb8ba0/cbor2-6.1.3-cp311-cp311-win32.whl", hash = "sha256:144f8cfd2e9149389c34026243aeb646184cd78a2c657822be9bc9e7a2c5f3f5", size = 282264, upload-time = "2026-07-04T10:36:01.131Z" }, + { url = "https://files.pythonhosted.org/packages/36/1f/e9d123a071ee67ebca70b37401a03b80641d86953a72e8ea41194f99095a/cbor2-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:187fd06befc59e6cafafc2709e5f1f3df8afe8bca5646f9cb5b70fc7e6ab1783", size = 303941, upload-time = "2026-07-04T10:36:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/32/4f/efb2ed376421641e372bfebe9fd98f11c9de3bbac1da9f2b8be5c96eb335/cbor2-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:43f0f694f47958de50fc84e6268a3015cc2a7fce88b231456c053bc5a1c6c828", size = 296378, upload-time = "2026-07-04T10:36:03.77Z" }, + { url = "https://files.pythonhosted.org/packages/31/16/cff14259c3d19a7f0ae88b6996fe4c85f6ff1764dad889ac8a39e843e39c/cbor2-6.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d939f55097c21e032f5a2d67592fcc57298986281f219356e2f519e4466f4ea", size = 412779, upload-time = "2026-07-04T10:36:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/50/6c/f3641d19b7b85a63cb2756c10164131489c2cb46b379ec51ae22283fefb9/cbor2-6.1.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b025009478d644dab407164fd60e3ef4381af284f5af6966df94c663756d949e", size = 457781, upload-time = "2026-07-04T10:36:06.349Z" }, + { url = "https://files.pythonhosted.org/packages/55/85/0c55a66f3037056bfb8e1c7184168085fdea67ae5830404498bcf466233b/cbor2-6.1.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2226d32e102e375737656ad5d141ad8c6ae3e705e04e263f24756f0eb379c6c1", size = 468373, upload-time = "2026-07-04T10:36:07.769Z" }, + { url = "https://files.pythonhosted.org/packages/46/74/40f7db3e0d880560193916a5c9b744fcf299558bed7113f77c28237c7c29/cbor2-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e61d465244d66ffed36492eef3b44d43795d76a2bba0663a2f15c186af7f7513", size = 523844, upload-time = "2026-07-04T10:36:09.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a1/b5e07d6a08441c3a552fe2ae48ccb7e9dfc5065b9f6a3bae9879b4f0fbc0/cbor2-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87fe7be8fab6ec4796aa127c1a52e09e79dbafd2aa31caf809cf04b8080a5975", size = 536238, upload-time = "2026-07-04T10:36:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/e166be0fd74bf3a91f5a0d103e34883efbc438d970f72cc8200e274787e5/cbor2-6.1.3-cp312-cp312-win32.whl", hash = "sha256:da25d345f01e6a40b2e5c57ef96b4dcff7be69394fb62f0f70e07f437f2376a9", size = 279858, upload-time = "2026-07-04T10:36:12.247Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/90b4a121e40aba189c55a5822dd3c698eaf487e1d4a780ab18c804a5ef1c/cbor2-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:d5514f693db6fa6f433b4096e9b604e6a7bf151c9ef1d2db86d0858e4c5e768f", size = 300929, upload-time = "2026-07-04T10:36:13.564Z" }, + { url = "https://files.pythonhosted.org/packages/19/db/52c58a8d33464927389dde8103997b3fa51b081ce29b347ac2cc4fd0dfbf/cbor2-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:3d43183d7beb3d3cd198d69b31bd2ee487ed704a1150c75cb0a66d6ad63d8c1a", size = 290908, upload-time = "2026-07-04T10:36:14.857Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8c/5024d623dcf3f2057ec8c991f584b939ba5f9025a5ce8c31f6fac067137a/cbor2-6.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:21c74b8ab67977c8b87b727247eeb730145b0068ad6d47f71e9f80f6b48c65f8", size = 412334, upload-time = "2026-07-04T10:36:16.299Z" }, + { url = "https://files.pythonhosted.org/packages/61/f3/e50654203c3b746166a96bea680eb6463b20c2c160cc14dfbe43f215ef6c/cbor2-6.1.3-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f291a0ae4c1ed96eadb0afa9752568c7424f7d6fa818676d5e33005fcd22ddd9", size = 457125, upload-time = "2026-07-04T10:36:17.684Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/6ef007f0d4f7afba90a80cb1657984de542e7474d2afaa7e920ac9860df3/cbor2-6.1.3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:dc8e44c7bf172687195dcd428157885bc00ea06efc0ea30fb371163b92bef733", size = 467651, upload-time = "2026-07-04T10:36:19.007Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/b5aa27813c02f37e03eb86bd908163562edd6fc7f99665bc7bbb25ef5e6c/cbor2-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf4d263983d830dd429d2b01f27be58ac02ba7c790c45d861f767eb63963e5", size = 523296, upload-time = "2026-07-04T10:36:20.504Z" }, + { url = "https://files.pythonhosted.org/packages/5b/46/e17b2bce2efdc26bfa045b4f6168f02923ac5f0e79732a1b3c42ce9ca9de/cbor2-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8719a7a2a2a82168844533389957b8f617a139f5f40e4d0ad7ed905fd3abebd1", size = 535537, upload-time = "2026-07-04T10:36:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/54/bc/add350acf37f367ae429f997f2e047b042f2d5ef9ca62461c923fbb0f3c3/cbor2-6.1.3-cp313-cp313-win32.whl", hash = "sha256:c73b54ce09dd8d522f3c1540426e36172ba0f34abf3d89eb93909a5e14590003", size = 279233, upload-time = "2026-07-04T10:36:23.071Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6f/1bbfce3b3131e4e03e8a86966a38ff92ebb72215fcf36aeecea1547f3e4b/cbor2-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:b77df56c462c10eb3444db8ef78d8c3e71d9ef8d021ea92e97c1f9e3aa918690", size = 300585, upload-time = "2026-07-04T10:36:24.563Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/3945702dd84b6e5b7800c9c7f1ada038d33d12d0042de10e38164cc03dfd/cbor2-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b144be2ab3e9584ee7b6359d2a92fee0a5bec1d00dbd34c215ccc2040ac0b2ab", size = 290357, upload-time = "2026-07-04T10:36:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/8f/40/04ad7d34182b27487a1824422b361b32d2607727ef20e563056eba62d12a/cbor2-6.1.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f3167ca9920b90db4ff72652db109dfd93b56ee0d583aca12f3a5d9a7019477", size = 414615, upload-time = "2026-07-04T10:36:27.299Z" }, + { url = "https://files.pythonhosted.org/packages/8e/29/94238c61f90653a606535e7509a2af312089fe10dafcb4cf82d6905a7a1a/cbor2-6.1.3-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:48c677971e4b71e685491e1a267d9924dc205e7ddbe3f34fd2562f16c0f6bfca", size = 459084, upload-time = "2026-07-04T10:36:28.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/6bd33461e8be8ded7ebb0fa38994a63752aefae2b4fcd1b2cc71ee3c06f1/cbor2-6.1.3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ad4f3c6dfc6b83331eb04c6975efb2839ab65a3aa81502bc2b3f7945d4c4aa44", size = 469310, upload-time = "2026-07-04T10:36:30.136Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d1/94195bcd8fcc1030ecaf22a7a825faa08891b5bb2d3553e465e50fe115f5/cbor2-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bcd609eab4a39745123bfb28e73311abba3d14975a87f5906e0e8b910d918ac", size = 524287, upload-time = "2026-07-04T10:36:31.453Z" }, + { url = "https://files.pythonhosted.org/packages/06/55/57178fbf2d1206af5299c688f9c917b83f30636694c8f932dc89c6652545/cbor2-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:672727ecb27d7fb3ca0bf8a58fc489d5374ab1fee680ed3d0348a11d9d3ca78f", size = 537031, upload-time = "2026-07-04T10:36:32.769Z" }, + { url = "https://files.pythonhosted.org/packages/fb/01/beefe26258d66ce39a9b057b679bc67dba81698d5a63e0ada5b4752a5c87/cbor2-6.1.3-cp314-cp314-win32.whl", hash = "sha256:1413cef2aa7f478a38298cd3492a055e8e8e45d17fb53bbe103e79ca15c33f3c", size = 286256, upload-time = "2026-07-04T10:36:34.078Z" }, + { url = "https://files.pythonhosted.org/packages/1b/2d/79eb513a2586a2053a6f18b33693beda65e2c766820d99676a306573fed5/cbor2-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:59df264d4a508ba61daaa0bf3c2f92d63275509549a0875c1fa38176f651e4f8", size = 313744, upload-time = "2026-07-04T10:36:35.58Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c7/ab9828e4efc26badf89f1397f866f3ef03ef65cdcd55518254b84bdaf86e/cbor2-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:b62b5d80a0eb4305cd5f0217faa4d7747bd64fe0dff9b88415e7be3782f8249b", size = 304280, upload-time = "2026-07-04T10:36:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/54a497ad1026833c1c92d482edbda0bdebb48314b51563d2fcea24ab89b4/cbor2-6.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:856ab525bfc599588b8d2a45babb7c3400c693ea0bb574d818467d997102fe24", size = 409638, upload-time = "2026-07-04T10:36:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/efdecd0848b9c9e43242537f6dd8ac5d441a077d362e5e6954c7775b866a/cbor2-6.1.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a076f35abf1dd0a4de6e2f7f4d4932abafc951a26275b6aa4a3b370c2fd3bbf4", size = 452193, upload-time = "2026-07-04T10:36:39.56Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bf/b5f43c75dc5f0ca3127919d3273d8671917932aa3e90767da63867e0f06f/cbor2-6.1.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:37ccab1d0bd3f57ff536a41e44165d0c99cb166ac6e5ffb8b93c42304b56e48d", size = 466614, upload-time = "2026-07-04T10:36:40.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/ee/e85b2ddd46b3b43e39a986704353b433efab0881234e1b4d824229fc2a75/cbor2-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76d257ce797e651fa430b0269e8e8c43549c54ce8b0d12860569b3709bf1326f", size = 518503, upload-time = "2026-07-04T10:36:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/60/13/c740c0002f127dc3e9e87b61a5d1285dd3b71aa11d8e0f6a408fc1f36173/cbor2-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4af35baadb66f7c9cbb3998eab469767c04641552416c1c69dd4d3d183797119", size = 534236, upload-time = "2026-07-04T10:36:43.641Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/a57d97177f3777c96f3406efb0ad60794fdbf249d497ec24559bfd1d0328/cbor2-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:61c92661665bccfed4ffa69d1fe10097f2c820d262f56a8cf909a5ebf9f6d8c6", size = 282446, upload-time = "2026-07-04T10:36:44.888Z" }, + { url = "https://files.pythonhosted.org/packages/11/c8/dd54878589df22c863d526cb82e1bb20e953d40223436449a52481c49804/cbor2-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:dc6bcd030bf5043662b84b0ca0f0ca942491bf509105db30cedca6e2ce82d158", size = 310300, upload-time = "2026-07-04T10:36:46.212Z" }, + { url = "https://files.pythonhosted.org/packages/05/d0/b0780a396d145e3356bfdc48578d021ade1818a6c7d7842a3d6bc6f16fbb/cbor2-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:da98a5e0ae9487bed497ac74e2c850b49975a3b7b5314b76c3843e2c83a6c8c4", size = 299391, upload-time = "2026-07-04T10:36:47.629Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -232,11 +280,11 @@ wheels = [ [[package]] name = "evaleval" -version = "0.2.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/39/b46bdf5496f2698390c54f5e3e2152d4ee596ba36da5e7bfffacd63d85e4/evaleval-0.2.7.tar.gz", hash = "sha256:9dc188f238e568f865f0278000e0dae03fb7ad351f3fe3145e281cc02a365362", size = 13869, upload-time = "2026-03-24T18:40:29.407Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/30/286684365064a472aee152bfee1964bfc4355b711585895fb08bb0d0deec/evaleval-0.2.7-py3-none-any.whl", hash = "sha256:1063435305cf3a29548cc48ec45bb8204de3b5b7c5ccc6f561b91d58b0bb4624", size = 11981, upload-time = "2026-03-24T18:40:28.326Z" }, +version = "0.4.0" +source = { git = "https://github.com/tommy-mor/evaleval.git?rev=584225b43f37261b446ad04169aaddf77ca6c201#584225b43f37261b446ad04169aaddf77ca6c201" } +dependencies = [ + { name = "cbor2" }, + { name = "rocksdict" }, ] [[package]] @@ -653,6 +701,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] +[[package]] +name = "rocksdict" +version = "0.3.29" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/2e/ab11dab0949e03abc3ceaa0f9c12486d121607a253a9cf82b997725aaac7/rocksdict-0.3.29-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:69f671398830c2b30e980d578c1c7e7cfe526ef2dc76df87d25cbe8c90a450b4", size = 4033999, upload-time = "2025-12-01T02:22:22.481Z" }, + { url = "https://files.pythonhosted.org/packages/2a/31/a7ba0efa92a6c96a991b142ab1d2aa9f5c973d51bc71028acdd3b50eed6e/rocksdict-0.3.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd7e3d765414d4469f9ac06cc411218cdff8e365f0aa91c726b70e61413a18e1", size = 3680830, upload-time = "2025-12-01T02:22:23.704Z" }, + { url = "https://files.pythonhosted.org/packages/b5/76/4de54c5db3f6ce39efa9bc015ee5ddad7aa1e29e7329df16ffc1a874b82a/rocksdict-0.3.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9a4290ffbdacfdd843b849ab1c82df661dcc0303bfb79f78249659454c29e1e", size = 4073265, upload-time = "2025-12-01T02:22:24.865Z" }, + { url = "https://files.pythonhosted.org/packages/45/c0/004e8242f91d1d89ce6264ff6ad79238e1fb570fcf95719adc54246a6ae6/rocksdict-0.3.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63a7d734e684bce1d2e102bbf0f443558d8f6d5d451472bbe18198decd3fa93b", size = 4215991, upload-time = "2025-12-01T02:22:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/96445cf2b838f87a9811bad75a18ed5b6eae2ac89e07b5bd0ec881400e3c/rocksdict-0.3.29-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f595d5438490450e2c6f797da491005e77c862f441ca2c4ed7346db6487a2983", size = 4027139, upload-time = "2025-12-01T02:22:27.706Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5d/95e2c6e2b6ae889eae0454a261fd57b72c30dec58b97a1ece8c09fa72114/rocksdict-0.3.29-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:63ebb60cde75a872c24b96837a9a373c68db229016a9284132ef06e0ee82d477", size = 4205331, upload-time = "2025-12-01T02:22:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/b8a12f414509c9d68c035e0264a9cd5a2b2bba6d5d768249ad783a2123f7/rocksdict-0.3.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:02c90d022b930d2f57578b91f5a12ee615cf3af346465fcfa7ada08421c67a59", size = 5068617, upload-time = "2025-12-01T02:22:31.185Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/b4e841d2060cd940bcb7a0d6fda9eb234587f4ea6d918f0aaf7e3dac603d/rocksdict-0.3.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9afa768ff0cb2d7fd1a3989684f7a93cfb2022f252ff48c3619abae99c0def57", size = 5311022, upload-time = "2025-12-01T02:22:32.776Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ca/881c29a98a9ab48f81aea810512c1757b12cb7942fbbd77ff406de95a064/rocksdict-0.3.29-cp311-cp311-win_amd64.whl", hash = "sha256:eed84c0bde6b9c40a016beb7f8003c8df9c95d128a72491f2d859f63dcf19477", size = 3895594, upload-time = "2025-12-01T02:22:34.674Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c6/98f7fc46694deecc1b2b3f3d87cdaf19dcb1f049fd42b9f69c25f79ec22b/rocksdict-0.3.29-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:226d6deac44d50a4539181789bb551e7f961d7d1a2e56df5b2e049e5e863b1a0", size = 4032457, upload-time = "2025-12-01T02:22:36.533Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/0ce238fb4d91c969e91606e18b2165e7ee831d13fb7a6575990b0ac02c7c/rocksdict-0.3.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d8c9a8f61d851f2f0c20452e321f3b999f85750119c91d41c09d6658d1caee97", size = 3678503, upload-time = "2025-12-01T02:22:37.664Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/f7e2b2e641a40c47a5a4180c5c80a240410156a669e310e5efec5200fcbb/rocksdict-0.3.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a987b63388dfb96de6ec10292611d696e3384c0e1361aa5b032e415008c2ff1", size = 4073331, upload-time = "2025-12-01T02:22:38.914Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/a56e673403a7db157e51af4debc2ed356947206f40331cae09a2992625ab/rocksdict-0.3.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:988dd3e2449d126b992057e03705efbb7ec92bf37918667000b80faac5e01482", size = 4216617, upload-time = "2025-12-01T02:22:40.419Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2a/cbdc8894f6fbe9083ec2da32584248d2dc5b691d97a92d7cc6a6745fabdc/rocksdict-0.3.29-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bec97acb9ac9797f26ecd75d25a3523c571907960c381c9dcab606979ac9c1c2", size = 4029010, upload-time = "2025-12-01T02:22:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/18cf95bf489ecde55f722d7a8e92e7f38677813bfe67239e25271c071d48/rocksdict-0.3.29-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:36d336d9d796d08923badb2df778365676cd5a66b4762ffc2d0b952dfca8b276", size = 4206954, upload-time = "2025-12-01T02:22:43.361Z" }, + { url = "https://files.pythonhosted.org/packages/74/d3/2e55fca42c1f3cd13233e7695dc941d6eae8fbb43d72952d993059c55655/rocksdict-0.3.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1ecc14495fbfddaaa87aacd48bf789184d9ac7d014e77a0b2ec2a15eac07ea5", size = 5069354, upload-time = "2025-12-01T02:22:44.716Z" }, + { url = "https://files.pythonhosted.org/packages/fc/42/c7eca5ca93b8cb2960b7ceb8a05f98771da55b1baf9eb7dced120470eabd/rocksdict-0.3.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afa57cdd72a4b83ed17648c5d4965d9e2951d4acfebc6b5d228c951a500002be", size = 5312781, upload-time = "2025-12-01T02:22:45.969Z" }, + { url = "https://files.pythonhosted.org/packages/37/83/9ba7eb587a7995c2581043d35fa3e63f4b1834b4e4f6da714b720b6425f4/rocksdict-0.3.29-cp312-cp312-win_amd64.whl", hash = "sha256:062c759fb15fe9e3699914790583eeac4031f4c89dc2f64aba503c3de6c21812", size = 3896188, upload-time = "2025-12-01T02:22:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/77/95/3658e1751381bb4e30df4733057f2feec883e3d74091feeb162548a70933/rocksdict-0.3.29-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:b881e786360e6caa12b29170cae2bbb4e7b95aff737b3f2c0426d4a66d322985", size = 4032054, upload-time = "2025-12-01T02:22:48.842Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0e/5764e0cca78d7b347a6908ec070297c2bc35b04b989a833d1ad1a0fdf513/rocksdict-0.3.29-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6195956c1ad600827ea1b782752031caba671073fa1b60d50991d07b596984ea", size = 3678609, upload-time = "2025-12-01T02:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/79b248fa1931324e4dcbce96a3208406272357372947e82d9ab15259e0f1/rocksdict-0.3.29-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4effb7f611243cb821d9be0b8115a9f5ab61dc0321245c2bb0501703ae7f89c0", size = 4072852, upload-time = "2025-12-01T02:22:51.274Z" }, + { url = "https://files.pythonhosted.org/packages/21/6b/f325aba1b5c5de58412fed4c67286577b782427fa3d0c0e3a80e79ddac9e/rocksdict-0.3.29-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66866e1226cd49e20e37125320893059c3f43307999b06e323971f23e7dd5219", size = 4216771, upload-time = "2025-12-01T02:22:52.593Z" }, + { url = "https://files.pythonhosted.org/packages/9d/05/af78d04750c9ad563d7a7d754b8d08930b5c2542426fc2eb367ec12916fa/rocksdict-0.3.29-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b286433f9ffbd7dbcc274f331f7e0ba5556f797fd9b3bcad46d2b42c4042066d", size = 4028614, upload-time = "2025-12-01T02:22:53.837Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e8/c3c6ed31d5ce89357dba065f4345c525488bee259585a174c49b81cc7f4c/rocksdict-0.3.29-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a9f1589664b138d92dfbd709714509c339dbacb096dc708b5cd1245f823ede58", size = 4206623, upload-time = "2025-12-01T02:22:55.133Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/d9e4cbc526dc424048d87b1ca282eb8bf0a847e6d2647f5cfd6fe25b2561/rocksdict-0.3.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d272421ed42a4dfbcaef61afb9799589a80e9329874ea76076470b03ec901c6a", size = 5069650, upload-time = "2025-12-01T02:22:56.399Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d2/06eaae0d4693b08640a01c5a41a2f41aba263ec49dc37d8387c64e5f8464/rocksdict-0.3.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb9af720898b7d2365e1577d46fc1f746d5a5bbe791d45462b33d30fd65d2a7", size = 5312282, upload-time = "2025-12-01T02:22:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/5b/59/f898bedf1fe8c688e13e27f42206bb620ae2d45882ab4aa5f1c4da6e59c7/rocksdict-0.3.29-cp313-cp313-win_amd64.whl", hash = "sha256:9b0ebdeb51210d8cc50a8c4bf86e0fd69d300b5ed322e5d1dd7ace7a9176d342", size = 3895350, upload-time = "2025-12-01T02:22:58.992Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/cfc0753de1e69bbda33b071bcda88f764c09de43c6ecdb45beefeadd9e50/rocksdict-0.3.29-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:449e5edc731018abcf043213ab97ccbbca81ba1c4041454847532c464dd460d2", size = 4046562, upload-time = "2025-12-01T02:23:00.291Z" }, + { url = "https://files.pythonhosted.org/packages/7e/63/e577b3033fb5e822ca4b7924fcf1cafaf48aa27a1ebed5d18a07991e392f/rocksdict-0.3.29-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fb12366bf75cb28c9126eec84524a9ba7d1a81934d6fec2b873ed6ed142361e6", size = 3691451, upload-time = "2025-12-01T02:23:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/d9/20/eb64c36439db85329ef46ccb02bd85562e5d1a090cbc0ea013ae3edcd7e5/rocksdict-0.3.29-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:961012099f10c146da68241af8e098cd7e534b705e7b2f2f629c242449ec2366", size = 4086360, upload-time = "2025-12-01T02:23:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/c891a3bd6e53eb1be6b36db6c4553ef72233e0b68b049eaffe4c0f0e8e1a/rocksdict-0.3.29-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e818b919531f38020cfcd055721361990eb22e5f4fbe64f08ec1708b4e9271", size = 4232136, upload-time = "2025-12-01T02:23:04.363Z" }, + { url = "https://files.pythonhosted.org/packages/90/d6/43f46cc9039138a04095816aa8c22904e65f3565278116cf3ccdbf9acb51/rocksdict-0.3.29-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:8bd5e4e3863f61e6f6537f6419e20f5d76051e72553a49353fbdc8c81ead0399", size = 4039371, upload-time = "2025-12-01T02:23:05.662Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d5/8e90cad5e1bc1e05a5125cbf9550fa73194c44599f7b1291a314b1191e5d/rocksdict-0.3.29-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:cb5f0a75a4d490822a4f09a76bc1fee24979c0ae04e0efb2a83aeb8f37457ef9", size = 4221400, upload-time = "2025-12-01T02:23:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/47/d651488f8827aa5fc1e54575de5c416ce12b08dd2aee5efe1077aa85c6b2/rocksdict-0.3.29-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:372a96002f412121fda7853810baa396f06356c30b90973ed64784318379b41a", size = 5081294, upload-time = "2025-12-01T02:23:08.577Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1e/0f4372b289ac549541b6546f4f2b0f65aee9640a346b7093c10cc4cdbfc9/rocksdict-0.3.29-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:092a0e6540de764bd196879bf2c93141e073e8f80f1b81a0b8ccb417d3e67cc8", size = 5326033, upload-time = "2025-12-01T02:23:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/10/09/8335ce02b9cf0f21a750ca4458bc2c052d7bd361057d2f6a0e869bdbeb43/rocksdict-0.3.29-cp314-cp314-win_amd64.whl", hash = "sha256:73ebb2ea670492c22a77b6042980953583cfcb2c62d2699f1afbbecdbe02cc6f", size = 3918558, upload-time = "2025-12-01T02:23:11.598Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/e4dfad14b3ab530a7b90477f686b0fb9fe4a64d3b1332ca6001b309d0d0d/rocksdict-0.3.29-pp311-pypy311_pp73-macosx_10_14_x86_64.whl", hash = "sha256:9bb766e3afb2092092edd6e3f9488d002c4ac216ce0143c2dd2b8cb3f696ccf4", size = 4033806, upload-time = "2025-12-01T02:23:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/8dcde086def3858953a72d48e3384abcb1b3a8ed726a24127a2cfbedb92a/rocksdict-0.3.29-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3e825e13ed532b30c38f7f927fc296dd2d72e619c5b766db8424a261874e7e87", size = 3682372, upload-time = "2025-12-01T02:23:51.646Z" }, + { url = "https://files.pythonhosted.org/packages/56/14/9bb2452779c62c83cab545e328d2a59283e8af945ee3fdcffcac6193e1f9/rocksdict-0.3.29-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e498312ff5203a2f41c58808259d4a40a78e28ebcc243a760ca8dea82ed13025", size = 4074851, upload-time = "2025-12-01T02:23:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8d/5534543cb018bf35a50a378e72c7189986a65427eec126503f0db9a30aa2/rocksdict-0.3.29-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e36663be98b9cbdab3b77fd655e91751e7444c8870d33e3de1935fc86171a01", size = 4217874, upload-time = "2025-12-01T02:23:54.607Z" }, + { url = "https://files.pythonhosted.org/packages/76/f5/4ce44a33d532500203723a58af3a78637ed1d4e2c13a0fb698cbc3438976/rocksdict-0.3.29-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1474ee839c65fc5cba5bab47ae5c73ce02118e30f681012c7e9b67db0ebbccca", size = 4030374, upload-time = "2025-12-01T02:23:55.943Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/43d1e599e7d54247fa6a4ee171161da1f93ba13aceb4171fcd98673c166f/rocksdict-0.3.29-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9b7a2d1aea49eca32dc76340b48b3b8b83788206b6769d8e4fda963130ee6f2", size = 4207638, upload-time = "2025-12-01T02:23:57.349Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/4f95c0b6fef23c83e4f05e14b5c56e9c59936ec88bb5eb858c832318d47d/rocksdict-0.3.29-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:5176a033a80e6f4009336e64f93f3a0c5e2d81927bf674c21755010200c8cf4e", size = 5071092, upload-time = "2025-12-01T02:23:59.135Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/981fe4e849b4b100b89057b8cb9b1ff8dfdf99792655bc8f7bba53a48c24/rocksdict-0.3.29-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:302b14fa11844247ef14365c58535c0ca80357b94d7c6b08a6ae3fc212eee2c1", size = 5313041, upload-time = "2025-12-01T02:24:00.553Z" }, + { url = "https://files.pythonhosted.org/packages/35/7d/395bc639e2552d657fbfaddebf0fc3d339a7fccb37fc9a84a0945f8562e4/rocksdict-0.3.29-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:701d81b041c948026e35baa762d0af5283d4ad976240c412861e5095d4f29bad", size = 3895088, upload-time = "2025-12-01T02:24:02.034Z" }, +] + [[package]] name = "slug-constitution" version = "0.1.0" @@ -667,6 +767,7 @@ dependencies = [ { name = "numpy" }, { name = "pytest" }, { name = "python-multipart" }, + { name = "rocksdict" }, { name = "starlette" }, { name = "sympy" }, { name = "tenacity" }, @@ -676,7 +777,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "authlib" }, - { name = "evaleval", specifier = "==0.2.7" }, + { name = "evaleval", git = "https://github.com/tommy-mor/evaleval.git?rev=584225b43f37261b446ad04169aaddf77ca6c201" }, { name = "fastapi" }, { name = "httpx" }, { name = "hypothesis" }, @@ -684,6 +785,7 @@ requires-dist = [ { name = "numpy" }, { name = "pytest" }, { name = "python-multipart" }, + { name = "rocksdict", specifier = ">=0.3.29" }, { name = "starlette" }, { name = "sympy" }, { name = "tenacity" },