-
Notifications
You must be signed in to change notification settings - Fork 6
NRCL-104 Add read-only paper journal queries #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+605
−1
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f07bc48
NRCL-104 Add read-only paper journal queries
05c533b
NRCL-104 Document journal query invariants
918beaf
NRCL-104 Preserve derived fee precision and SQLite contention
0a1e5fe
NRCL-104 Preserve fair comparison inputs and result identities
3aa452f
NRCL-104 Reject incomplete fees and malformed result storage
22d8800
NRCL-104 Harden stored data and archive read boundaries
3747432
NRCL-104 Validate finite numbers and saved history consistency
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| """Read-only views of Neural's local paper journal; never run or migrate jobs.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import json | ||
| import math | ||
| import re | ||
| import sqlite3 | ||
| from collections.abc import Iterator | ||
| from contextlib import contextmanager | ||
| from decimal import Decimal, localcontext | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| from neural.paper_worker import APPLICATION_ID, MAX_INPUT_BYTES | ||
| from neural.sports import SportsMarket, compare_sports_markets | ||
| from neural.strategy import StrategySpec, _decimal_text | ||
|
|
||
|
|
||
| class PaperJournal: | ||
| """Inspect an existing v1 journal without a writer lock or worker startup. | ||
|
|
||
| Missing files raise FileNotFoundError; malformed/unsupported journals and | ||
| inputs raise ValueError. Historical model results remain opaque on reads. | ||
| The caller owns the journal path and selects the verified runtime archive. | ||
| """ | ||
|
|
||
| def __init__(self, database: str | Path): | ||
| """Select an existing local journal path without opening or creating it.""" | ||
| if str(database) in ("", ":memory:"): | ||
| raise ValueError("paper journal requires an existing database file") | ||
| self.database = Path(database).resolve() | ||
|
|
||
| @contextmanager | ||
| def _connection(self) -> Iterator[sqlite3.Connection]: | ||
| """Open a read snapshot and reject journals outside the supported schema.""" | ||
| if not self.database.exists(): | ||
| raise FileNotFoundError("paper job database not found") | ||
| with self.database.open("rb") as source: | ||
| header = source.read(20) | ||
| if header.startswith(b"SQLite format 3\x00") and header[18:20] != b"\x01\x01": | ||
| raise ValueError("WAL paper journals are unsupported; use a rollback-journal archive") | ||
| db = None | ||
| try: | ||
| db = sqlite3.connect(self.database.as_uri() + "?mode=ro", uri=True, timeout=5) | ||
| db.row_factory = sqlite3.Row | ||
| db.execute("PRAGMA query_only=ON") | ||
| db.execute("BEGIN") | ||
| if ( | ||
| db.execute("PRAGMA application_id").fetchone()[0] != APPLICATION_ID | ||
| or db.execute("PRAGMA user_version").fetchone()[0] != 1 | ||
| ): | ||
| raise ValueError("unsupported paper job database") | ||
| yield db | ||
| except sqlite3.Error as exc: | ||
| if isinstance(exc, sqlite3.OperationalError) and str(exc) in ( | ||
| "database is locked", | ||
| "database table is locked", | ||
| ): | ||
| raise | ||
| raise ValueError("cannot read paper job database") from exc | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| finally: | ||
| if db is not None: | ||
| db.close() | ||
|
|
||
| @staticmethod | ||
| def _reject_constant(value: str) -> Any: | ||
| """Reject nonstandard JSON numbers that cannot be written by the worker.""" | ||
| raise ValueError("stored job JSON contains a non-finite constant") | ||
|
|
||
| @staticmethod | ||
| def _finite_float(value: str) -> float: | ||
| """Decode standard JSON numbers without allowing floating-point overflow.""" | ||
| number = float(value) | ||
| if not math.isfinite(number): | ||
| raise ValueError("stored job JSON contains a non-finite number") | ||
| return number | ||
|
|
||
| @staticmethod | ||
| def _view(row: sqlite3.Row) -> dict[str, Any]: | ||
| """Decode a saved row while checking configuration identity and state.""" | ||
| try: | ||
| if not isinstance(row["config"], str): | ||
| raise ValueError("stored job configuration is invalid") | ||
| config = json.loads( | ||
| row["config"], | ||
| parse_constant=PaperJournal._reject_constant, | ||
| parse_float=PaperJournal._finite_float, | ||
| ) | ||
| if ( | ||
| not isinstance(config, dict) | ||
| or hashlib.sha256(row["config"].encode()).hexdigest() != row["id"] | ||
| or row["status"] not in ("queued", "completed", "failed") | ||
| ): | ||
| raise ValueError("stored job input integrity mismatch") | ||
| view = {key: row[key] for key in ("id", "status", "error")} | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| if (row["error"] is not None and not isinstance(row["error"], str)) or ( | ||
| (row["status"] == "failed") != (row["error"] is not None) | ||
| ): | ||
| raise ValueError("stored job error is invalid") | ||
| view["config"] = config | ||
| if "has_result" in row.keys() and ( | ||
| (row["status"] == "completed") != bool(row["has_result"]) | ||
| ): | ||
| raise ValueError("stored job result is invalid") | ||
| if "result" in row.keys(): | ||
| if row["result"] is not None and not isinstance(row["result"], str): | ||
| raise ValueError("stored job result is invalid") | ||
| result = ( | ||
| json.loads( | ||
| row["result"], | ||
| parse_constant=PaperJournal._reject_constant, | ||
| parse_float=PaperJournal._finite_float, | ||
| ) | ||
| if row["result"] is not None | ||
| else None | ||
| ) | ||
| if (result is not None and not isinstance(result, dict)) or ( | ||
| (row["status"] == "completed") != (result is not None) | ||
| ): | ||
| raise ValueError("stored job result is invalid") | ||
| view["result"] = result | ||
| return view | ||
| except (TypeError, json.JSONDecodeError) as exc: | ||
| raise ValueError("stored job data is invalid") from exc | ||
|
|
||
| @classmethod | ||
| def _job( | ||
| cls, db: sqlite3.Connection, identity: str, *, include_result: bool = True | ||
| ) -> dict[str, Any]: | ||
| """Read one saved job, optionally excluding its unrelated output.""" | ||
| if not isinstance(identity, str): | ||
| raise ValueError("job identity must be a string") | ||
| fields = "id,status,error,config,result" if include_result else "id,status,error,config" | ||
| row = db.execute(f"SELECT {fields} FROM jobs WHERE id=?", (identity,)).fetchone() | ||
| if row is None: | ||
| raise ValueError("unknown paper job") | ||
| return cls._view(row) | ||
|
|
||
| def history(self, *, offset: int = 0, limit: int = 25) -> dict[str, Any]: | ||
| """Newest inserted jobs first, without result traces or invented timestamps.""" | ||
| if type(offset) is not int or not 0 <= offset <= 100000: | ||
| raise ValueError("offset must be an integer from 0 to 100000") | ||
| if type(limit) is not int or not 1 <= limit <= 100: | ||
| raise ValueError("limit must be an integer from 1 to 100") | ||
| with self._connection() as db: | ||
| rows = db.execute( | ||
| "SELECT id,status,error,config,result IS NOT NULL AS has_result " | ||
| "FROM jobs ORDER BY rowid DESC LIMIT ? OFFSET ?", | ||
| (limit + 1, offset), | ||
| ).fetchall() | ||
| return { | ||
| "jobs": [self._view(row) for row in rows[:limit]], | ||
| "has_more": len(rows) > limit, | ||
| } | ||
|
|
||
| def inspect(self, identity: str) -> dict[str, Any]: | ||
| """Return saved configuration and result; do not execute or reinterpret it.""" | ||
| with self._connection() as db: | ||
| return self._job(db, identity) | ||
|
|
||
| def recording(self, identity: str) -> bytes: | ||
| """Return original input bytes after checking size and the saved digest.""" | ||
| with self._connection() as db: | ||
| job = self._job(db, identity, include_result=False) | ||
| raw = db.execute( | ||
| "SELECT CASE WHEN length(recording) BETWEEN 1 AND ? THEN recording END " | ||
| "FROM jobs WHERE id=?", | ||
| (MAX_INPUT_BYTES, identity), | ||
| ).fetchone()[0] | ||
| if not isinstance(raw, bytes) or hashlib.sha256(raw).hexdigest() != job["config"].get( | ||
| "recording_sha256" | ||
| ): | ||
| raise ValueError("stored job recording integrity mismatch") | ||
| return raw | ||
|
|
||
| def _pair(self, first: str, second: str) -> list[dict[str, Any]]: | ||
| """Read two distinct completed jobs from a single consistent snapshot.""" | ||
| if first == second: | ||
| raise ValueError("choose two distinct completed experiments") | ||
| with self._connection() as db: | ||
| jobs = [self._job(db, identity) for identity in (first, second)] | ||
| if any(job["status"] != "completed" for job in jobs): | ||
| raise ValueError("choose two distinct completed experiments") | ||
| return jobs | ||
|
|
||
| @staticmethod | ||
| def _assumptions(job: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: | ||
| """Separate fixed replay inputs from tunable strategy thresholds and caps.""" | ||
| config = dict(job["config"]) | ||
| strategy = config.pop("strategy", None) | ||
| StrategySpec.from_dict(strategy) | ||
| return config, { | ||
| key: strategy[key] | ||
| for key in ("schema_version", "kind", "venue", "market_id", "outcome", "quantity") | ||
| } | ||
|
hudsonaikins marked this conversation as resolved.
|
||
|
|
||
| def compare(self, first: str, second: str) -> list[dict[str, Any]]: | ||
| """Compare strategy variants only when their recorded inputs/assumptions match.""" | ||
| jobs = self._pair(first, second) | ||
| if self._assumptions(jobs[0]) != self._assumptions(jobs[1]): | ||
| raise ValueError( | ||
| "comparison requires the same recording, model, market, outcome, " | ||
| "quantity, cash, fees and simulation limits" | ||
| ) | ||
| for job in jobs: | ||
| trace = job["result"].get("trace") | ||
| if not isinstance(trace, list) or any( | ||
| not isinstance(event, dict) or not isinstance(event.get("action"), str) | ||
| for event in trace | ||
| ): | ||
| raise ValueError("saved comparison trace unavailable or invalid") | ||
| with localcontext() as context: | ||
| context.prec = 100 | ||
| total_fees = sum((self._trace_fee(event) for event in trace), Decimal(0)) | ||
| job["summary"] = { | ||
| "total_fees": _decimal_text(total_fees), | ||
| "fill_count": sum(event["action"] == "fill" for event in trace), | ||
| "rejection_count": sum(event["action"] == "reject" for event in trace), | ||
| } | ||
| return jobs | ||
|
|
||
| @staticmethod | ||
| def _trace_fee(event: dict[str, Any]) -> Decimal: | ||
| """Parse products of two 18-digit inputs without truncating their scale.""" | ||
| value = event.get("fees") if event["action"] == "fill" else event.get("fees", "0") | ||
|
hudsonaikins marked this conversation as resolved.
|
||
| if ( | ||
| not isinstance(value, str) | ||
| or re.fullmatch(r"(0|[1-9][0-9]{0,35})(\.[0-9]{1,36})?", value) is None | ||
| ): | ||
| raise ValueError("saved trace fees must be a finite nonnegative decimal product") | ||
| fee = Decimal(value) | ||
| if event["action"] != "fill" and fee != 0: | ||
| raise ValueError("saved trace fees require a fill event") | ||
| return fee | ||
|
|
||
| def compare_markets(self, first: str, second: str) -> dict[str, Any]: | ||
| """Inspect sports terms separately; this never compares execution costs/PnL.""" | ||
| jobs = self._pair(first, second) | ||
| markets = [job["result"].get("sports_market") for job in jobs] | ||
| if any(market is None for market in markets): | ||
| raise ValueError( | ||
| "market terms unavailable: both recordings need sports contract metadata" | ||
| ) | ||
| comparison = compare_sports_markets(*(SportsMarket.from_dict(market) for market in markets)) | ||
| return {"comparison": comparison.to_dict(), "ids": [first, second], "markets": markets} | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.