"""Independent invariant checks (spec 18.3). All checks are implemented against the specification text; they never call producer validation functions. Each returns (ok, details). """ from ..util import sha256_bytes def check_monotonic_stream(records): """ts_ms must be non-decreasing across the merged stream.""" prev = None bad = 0 first_bad = None for rec in records: ts = rec[0] if prev is not None and ts < prev: bad += 1 if first_bad is None: first_bad = (prev, ts) prev = ts return bad == 0, {"regressions": bad, "first": first_bad} def check_ohlc_validity(bar_rows): """low <= open,close <= high; counts >= 1; avg = sum // count.""" bad = 0 for row in bar_rows: (bar_idx, _pid, _st, _en, o, h, l, c, n, _v, _smn, _smx, ss, avg, _final, _f, _l) = row if not (l <= o <= h and l <= c <= h and n >= 1): bad += 1 if avg != (ss // n): bad += 1 return bad == 0, {"invalid": bad} def check_bar_uniqueness(bar_rows): """(TF period) uniqueness is implied per stream; bar_idx must be strictly increasing; period_id strictly increasing within one stream.""" seen = {} bad = 0 for row in bar_rows: bar_idx, period_id = row[0], row[1] if seen.get(period_id): bad += 1 seen[period_id] = True return bad == 0, {"duplicates": bad} def check_chunk_counts_sum(per_chunk_counts, total): return sum(per_chunk_counts) == total, {"sum": sum(per_chunk_counts), "total": total} def check_carry_continuity(carry, first_tick): """Resume continuity: first ts must be >= carried last_ts; carried period must not be re-emitted.""" if carry is None: return True, {} if first_tick[0] < carry["last_ts_ms"]: return False, {"first_ts": first_tick[0], "carried_last": carry["last_ts_ms"]} return True, {} def independent_serialize_ticks(records): """Independent canonical tick serialization (spec 8.5). ``records`` is an iterable of (ts_ms, bid_u, ask_u, vol) tuples in rec_ord order.""" from .vaggregate import v_tick_serialize return b"".join(v_tick_serialize(r[0], r[1], r[2], r[3]) for r in records) def independent_ticks_content_id(records): return sha256_bytes(independent_serialize_ticks(records)) def independent_bars_content_id(rows): from .vaggregate import v_bar_serialize return sha256_bytes(b"".join(v_bar_serialize(r) for r in rows)) def independent_preservation_content_id(pairs): """Independent source-preservation content id (spec 8.6). ``pairs`` is an iterable of (src_line, last_u) integers in rec_ord order, extracted independently from the sidecar; the verifier re-derives the identity from its own row extraction with the fixed canonical serialization ``src_line|last_u\n`` (no floats).""" parts = [] for src_line, last_u in pairs: parts.append(("%d|%d" % (src_line, last_u)).encode("ascii") + b"\n") return sha256_bytes(b"".join(parts))