From 24e06e71a93d0dc9774b037c0327c3c41f820333 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 07:06:27 +1000 Subject: [PATCH 1/5] refactor(tools): fold generate-bin-twin into upsert-by-name generate (LAB-1203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit python-frame-reference.py 'generate' rewrote the whole fixture from whatever the installed wheel could produce, so every vector the wheel could not rebuild was one guard away from silent deletion — LAB-903 found exactly that as a CRIT and PR #47 patched it with a drop-refusal. Upsert-by-name dissolves the hazard instead of guarding it: only vectors the wheel reproduces are rewritten (matched by name), everything else stays byte-untouched, so dropping a committed vector is structurally impossible and the drop guard, both wheel-direction refusals, and the generate-bin-twin entry point are deleted. The wheel's envelope encoding now selects WHICH default-path vector it rebuilds: a protocol 1.1 (bin) wheel upserts the _bin twin, a legacy wheel the legacy original. The pair is still proven to differ only in envelope encoding before anything is written (the LAB-903 twin-lie protection), and rewritten vectors carry per-vector generator provenance; a no-op run never rewrites the file. The ByteStorage envelope codec is no longer reimplemented: encode/decode come from wire-format-reference.py (stdlib-only, so 'verify' stays dependency-free), with a new generation-time fidelity check that the shared encoder reproduces the wheel's envelope byte-identically. This also makes the Python verifier enforce the protocol 1.1 flip exclusions (checksum stays int-array, format stays fixstr) that previously only the Node cross-check enforced. Verified: fixture byte-unchanged; no-op generate under cachekit 0.17.1 byte-identical; subset run (no pyarrow) preserves arrow vector; six-class mutation suite still fails in BOTH independent verifiers; full local verify.yml suite green. --- CHANGELOG.md | 14 ++ tools/python-frame-reference.py | 363 +++++++++++++++----------------- 2 files changed, 185 insertions(+), 192 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeecdc4..7ab8ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,20 @@ All notable changes to the CacheKit Protocol Specification. ### Test Vectors +- `tools/python-frame-reference.py generate` now **upserts by vector name** + (LAB-1203): it rebuilds only the vectors the installed `cachekit` wheel can + reproduce and leaves every other committed vector byte-untouched, so dropping + a committed vector is structurally impossible — which deletes the LAB-903 + drop-refusal guard and both wheel-direction refusals, and folds the + append-only `generate-bin-twin` mode into `generate` (a protocol 1.1 wheel + rebuilds the `_bin` twin, a legacy wheel the legacy original; the default-path + pair is still proven to differ only in envelope encoding before writing). + The ByteStorage envelope codec is no longer reimplemented there: encode/decode + come from `tools/wire-format-reference.py`, the one shared implementation of + the encoding these fixtures pin. Rewritten vectors carry per-vector + `generator` provenance; `test-vectors/python-frame.json` is byte-unchanged by + this refactor, and a no-op `generate` never rewrites the file. + - 7 legacy/`bin` vector pairs in `test-vectors/wire-format.json` (append-only; legacy vectors are retained forever as legacy-read proof; fixture 1.0.0 → 1.1.1). The original six `bin` twins were generated by the stdlib-only diff --git a/tools/python-frame-reference.py b/tools/python-frame-reference.py index a9ec739..4928769 100644 --- a/tools/python-frame-reference.py +++ b/tools/python-frame-reference.py @@ -13,33 +13,36 @@ independent minimal parser (no cachekit import) and checks the expected header/payload; checks every error vector is rejected. Runs in CI. - generate Regenerates the vector file. Requires the real `cachekit` - package (PyPI wheel with the Rust core) plus `msgpack`; - the Arrow vector additionally needs `pyarrow` + `pandas`. - Every generated frame is round-tripped through the real - cachekit-py deserialization path before being written. - Rewrites the whole fixture, so it refuses to write if that - would drop any already-committed vector — the legacy vectors - were generated by cachekit 0.11.1 (array-of-ints envelopes) - and no post-0.4.0-core wheel (bin envelopes) can reproduce - them. Use generate-bin-twin for post-protocol-1.1 updates. - - generate-bin-twin - Append-only: generates ONLY the protocol 1.1 `bin`-envelope - twin of the default-path vector from the installed `cachekit` - wheel and appends it to the existing fixture, leaving every - other vector byte-untouched (LAB-903). Requires `cachekit` + - `msgpack`; refuses to write if the wheel still emits - array-of-ints envelopes. + generate Upserts the vector file by vector name (LAB-1203): every vector + the installed wheel can reproduce is rebuilt, and rewritten only + if its content actually changed; every other committed vector is + left byte-untouched. The fixture is edited in place, never + rebuilt from scratch, so dropping a committed vector is + structurally impossible. Requires the real `cachekit` package + (PyPI wheel with the Rust core) plus `msgpack`; the Arrow vector + is rebuilt only when `pyarrow` + `pandas` are importable and is + otherwise left as committed. A wheel emitting protocol 1.1 `bin` + envelopes rebuilds the `_bin` twin of the default-path vector; a + legacy (array-of-ints) wheel rebuilds the legacy original — the + vector a wheel cannot produce is simply not touched. Every + generated frame is round-tripped through the real cachekit-py + deserialization path before being written, and the default-path + pair must differ ONLY in envelope encoding. The independent parser below implements exactly the layout documented in spec/wire-format.md: MAGIC b"CK" | VERSION u8 (=3) | HDR_LEN u32-BE | HEADER (UTF-8 JSON) | PAYLOAD + +The ByteStorage envelope codec is NOT reimplemented here: encode/decode come +from tools/wire-format-reference.py, the single shared implementation of the +encoding these fixtures exist to pin (stdlib-only, so `verify` stays +dependency-free). """ from __future__ import annotations +import importlib.util import json import sys from pathlib import Path @@ -51,6 +54,20 @@ PREFIX_LEN = 7 # magic(2) + version(1) + header_len(4) +def _load_wire_format_codec(): + """Load tools/wire-format-reference.py as a module (hyphenated filename).""" + path = Path(__file__).resolve().parent / "wire-format-reference.py" + spec = importlib.util.spec_from_file_location("wire_format_reference", path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load envelope codec from {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +_wire = _load_wire_format_codec() + + class FrameError(ValueError): pass @@ -94,29 +111,6 @@ def parse_frame(frame: bytes) -> tuple[dict, bytes]: return header, frame[header_end:] -#: msgpack tags a ByteStorage envelope's compressed_data field can legally carry. -#: bin8/bin16/bin32 are the protocol 1.1 canonical encoding; fixarray/array16/ -#: array32 are the legacy array-of-ints encoding readers must accept forever. -_BIN_TAGS = (0xC4, 0xC5, 0xC6) -_INT_ARRAY_TAGS = (0xDC, 0xDD, *range(0x90, 0xA0)) - - -def _envelope_encoding(payload: bytes) -> str: - """Classify an envelope's compressed_data encoding from its msgpack tag. - - Stdlib-only, so the no-dependency CI leg proves the protocol 1.1 dual-read - property on its own instead of delegating it to the Node cross-check. - """ - if len(payload) < 2 or payload[0] != 0x94: - raise FrameError(f"envelope is not a msgpack fixarray(4) (leading byte 0x{payload[:1].hex()})") - tag = payload[1] - if tag in _BIN_TAGS: - return "bin" - if tag in _INT_ARRAY_TAGS: - return "int-array" - raise FrameError(f"compressed_data tag 0x{tag:02x} is neither msgpack bin nor an array") - - def verify() -> int: doc = _load_fixture() failures = 0 @@ -145,10 +139,16 @@ def verify() -> int: print(f"FAIL {name}: payload_envelope must declare envelope_encoding ('bin' or 'int-array')") vec_failed += 1 else: + # Shared, stdlib-only codec (wire-format-reference.py), so the + # no-dependency CI leg proves the protocol 1.1 dual-read + # property on its own instead of delegating it to the Node + # cross-check. decode_envelope also enforces the exclusions + # from the 1.1 flip: checksum stays an array of 8 integers and + # format stays a fixstr, in BOTH encodings. try: - actual = _envelope_encoding(payload) - except FrameError as e: - print(f"FAIL {name}: {e}") + *_, actual = _wire.decode_envelope(payload) + except ValueError as e: + print(f"FAIL {name}: envelope decode: {e}") vec_failed += 1 else: if actual != declared: @@ -208,13 +208,14 @@ def verify() -> int: return 0 -def _build_default_path_vector() -> tuple[dict, str]: +def _build_default_path_vector() -> dict: """Build the default-@cache-write vector from the installed cachekit wheel. - Returns (vector_dict, envelope_encoding) where envelope_encoding is - "bin" (msgpack 0xc4/0xc5/0xc6, protocol 1.1 writers) or "int-array" - (legacy array-of-ints writers). Every frame is round-tripped through the - real cachekit-py deserialization path before being returned. + The vector's name follows the envelope encoding the wheel emits: + "bin" (msgpack 0xc4/0xc5/0xc6, protocol 1.1 writers) builds the `_bin` + twin, "int-array" (legacy array-of-ints writers) builds the legacy + original. Every frame is round-tripped through the real cachekit-py + deserialization path before being returned. """ import msgpack # third-party; generation only @@ -231,57 +232,119 @@ def _build_default_path_vector() -> tuple[dict, str]: inner, fmt = ByteStorage("msgpack").retrieve(payload) inner = bytes(inner) _require(msgpack.unpackb(inner) == value and fmt == "msgpack", "ByteStorage.retrieve round-trip mismatch") - env = msgpack.unpackb(payload) # positional fixarray(4) - _require(isinstance(env, list) and len(env) == 4, "envelope is not a 4-element msgpack array") - # msgpack-python decodes bin as bytes and array-of-ints as list — the - # observed type IS the wire encoding of compressed_data. - encoding = "bin" if isinstance(env[0], bytes) else "int-array" - # Protocol 1.1 scopes the bin flip to compressed_data ONLY: checksum - # [u8;8] must stay an array of integers in every encoding. - _require(isinstance(env[1], list), "checksum drifted to msgpack bin (excluded from the protocol 1.1 flip)") - # `format` is excluded alongside `checksum`, and it feeds AAD construction — - # a drift to bin here must be a named invariant, not a json.dumps TypeError. - _require(isinstance(env[3], str), "format drifted off msgpack str (excluded from the protocol 1.1 flip)") + # Shared codec (wire-format-reference.py). decode_envelope enforces the + # protocol 1.1 flip exclusions — checksum must stay an array of 8 integers + # and format a fixstr — so a wheel drifting either field fails here. + data, checksum, original_size, env_fmt, encoding = _wire.decode_envelope(payload) + _require(env_fmt == fmt, "envelope format field disagrees with ByteStorage.retrieve") + # Codec fidelity against the real wheel: the shared encoder must reproduce + # the wheel's envelope byte-identically, or codec and wheel have drifted. + _require( + _wire.encode_envelope(data, checksum, original_size, env_fmt, encoding=encoding) == payload, + "shared envelope codec does not reproduce the wheel's envelope bytes", + ) default_header, _ = parse_frame(frame) _require(default_header["m"] == meta and default_header["s"] == ser_name, "frame header disagrees with unwrap metadata") - vector = { - "name": "default_saas_write_msgpack_bytestorage", - "description": ( + if encoding == "bin": + name = "default_saas_write_msgpack_bytestorage_bin" + description = ( + "Protocol 1.1 twin of default_saas_write_msgpack_bytestorage: same value, same " + "default @cache write path, but the ByteStorage envelope's compressed_data is " + "msgpack bin (serde_bytes) instead of an array of integers. Readers MUST accept " + "both encodings; the legacy encoding stays pinned by the legacy vector's bytes." + ) + encoding_note = ( + "rmp_serde positional fixarray(4); compressed_data encodes as msgpack bin " + "(serde_bytes, protocol 1.1); checksum [u8;8] stays an array of integers" + ) + else: + name = "default_saas_write_msgpack_bytestorage" + description = ( "Exact stored bytes for a default @cache write (StandardSerializer, integrity on): " "CK v3 frame wrapping the ByteStorage envelope of the MessagePack-encoded value. " "This is what any backend — including the SaaS — receives from cachekit-py in auto mode." - ), + ) + encoding_note = ( + "rmp_serde::to_vec positional fixarray(4); Vec/[u8;8] fields encode as msgpack arrays of integers" + ) + return { + "name": name, + "description": description, "value_json": value, "frame_hex": frame.hex(), "expected_header": default_header, "expected_payload_hex": payload.hex(), "payload_envelope": { - "encoding": ( - "rmp_serde positional fixarray(4); compressed_data encodes as msgpack bin " - "(serde_bytes, protocol 1.1); checksum [u8;8] stays an array of integers" - if encoding == "bin" - else "rmp_serde::to_vec positional fixarray(4); Vec/[u8;8] fields encode as msgpack arrays of integers" - ), + "encoding": encoding_note, "envelope_encoding": encoding, - "compressed_data_hex": bytes(env[0]).hex(), - "checksum_hex": bytes(env[1]).hex(), - "original_size": env[2], - "format": env[3], + "compressed_data_hex": data.hex(), + "checksum_hex": checksum.hex(), + "original_size": original_size, + "format": env_fmt, "inner_msgpack_hex": inner.hex(), }, } - return vector, encoding + + +def _upsert(committed: list[dict], built: list[dict], generator_stamp: str) -> int: + """Replace committed vectors the wheel rebuilt (matched by name); append new names. + + Never removes anything: a vector this run did not rebuild stays exactly as + committed, so dropping a committed vector is structurally impossible. A + rebuilt vector whose content matches the committed one (ignoring its + per-vector 'generator' provenance) keeps the committed entry byte-untouched + — a no-op `generate` leaves the fixture byte-identical. Returns the number + of vectors actually rewritten or added. + """ + index = {v["name"]: i for i, v in enumerate(committed)} + _require(len(index) == len(committed), "committed fixture has duplicate vector names") + changed = 0 + for vec in built: + i = index.get(vec["name"]) + if i is not None and {k: v for k, v in committed[i].items() if k != "generator"} == vec: + continue + stamped = {**vec, "generator": generator_stamp} + if i is None: + index[vec["name"]] = len(committed) + committed.append(stamped) + else: + committed[i] = stamped + changed += 1 + return changed + + +def _require_twin_equivalence(frame_vectors: list[dict]) -> None: + """The default-path pair must differ ONLY in envelope encoding. + + The `_bin` twin's description asserts the encoding is the sole delta from + the legacy vector. Prove it rather than trusting the wheel: a wheel that + also changed the LZ4 level, msgpack key order, or the frame header would + upsert a vector that lies about what it isolates, into a fixture + downstream SDKs pin (LAB-903). + """ + by_name = {v["name"]: v for v in frame_vectors} + legacy = by_name.get("default_saas_write_msgpack_bytestorage") + twin = by_name.get("default_saas_write_msgpack_bytestorage_bin") + _require(legacy is not None and twin is not None, "default-path vector pair incomplete") + assert legacy is not None and twin is not None # narrowing; _require already raised + _require(twin["value_json"] == legacy["value_json"], "twin value_json differs from the legacy vector") + _require(twin["expected_header"] == legacy["expected_header"], "twin frame header differs from the legacy vector") + for field in ("compressed_data_hex", "checksum_hex", "original_size", "format", "inner_msgpack_hex"): + _require( + twin["payload_envelope"][field] == legacy["payload_envelope"][field], + f"twin payload_envelope.{field} differs from the legacy vector — encoding must be the ONLY delta", + ) def generate() -> int: import msgpack # third-party; generation only - from cachekit.cache_handler import CacheSerializationHandler from cachekit.serializers.wrapper import SerializationWrapper import cachekit - vectors: list[dict] = [] + doc = _load_fixture() + built: list[dict] = [] # 1. Minimal parse vector: real SerializationWrapper.wrap over known raw bytes. raw_payload = b"hello, cachekit!" @@ -293,7 +356,7 @@ def generate() -> int: # vector pins what the frame actually contains (incl. the "v" field, which # cachekit-py's unwrap drops) rather than a hand-maintained copy. raw_header, _ = parse_frame(raw_frame) - vectors.append( + built.append( { "name": "raw_payload_frame", "description": "Minimal frame: SerializationWrapper.wrap over raw bytes. Parse-level vector.", @@ -303,24 +366,21 @@ def generate() -> int: } ) - # 2. Full default-path SaaS write: value -> msgpack -> ByteStorage envelope -> CK frame. - default_vector, encoding = _build_default_path_vector() - if encoding != "int-array": - print( - f"REFUSED: installed cachekit {cachekit.__version__} emits {encoding} envelopes; " - "'generate' cannot reproduce the legacy (array-of-ints) vectors. Use " - "'generate-bin-twin' instead. Nothing written.", - file=sys.stderr, - ) - return 1 - vectors.append(default_vector) + # 2. Full default-path SaaS write: value -> msgpack -> ByteStorage envelope + # -> CK frame. Named by the envelope encoding the wheel emits, so a + # protocol 1.1 wheel rebuilds the _bin twin and a legacy wheel rebuilds the + # legacy original — either way the other vector stays as committed. + built.append(_build_default_path_vector()) # 3. Arrow path: frame wrapping [8-byte xxHash3-64][Arrow IPC file]. - # Hard requirement for generation — writing the fixture without this vector - # would silently shrink the committed vector set. + # Optional: without pandas + pyarrow the committed vector is left untouched. try: import pandas as pd import pyarrow + except ImportError as exc: + print(f"note: pandas/pyarrow not importable ({exc}); arrow_dataframe_write left as committed", file=sys.stderr) + else: + from cachekit.cache_handler import CacheSerializationHandler arrow_handler = CacheSerializationHandler(serializer_name="arrow") df = pd.DataFrame({"id": [1, 2], "score": [1.5, 2.5]}) @@ -332,7 +392,7 @@ def generate() -> int: _require(a_payload[8:14] == b"ARROW1", "Arrow IPC magic not at documented offset") arrow_header, _ = parse_frame(arrow_frame) _require(arrow_header["m"] == a_meta and arrow_header["s"] == a_ser, "Arrow frame header disagrees with unwrap metadata") - vectors.append( + built.append( { "name": "arrow_dataframe_write", "description": ( @@ -351,16 +411,9 @@ def generate() -> int: }, } ) - except ImportError as exc: - print( - "generate requires pandas + pyarrow (the arrow_dataframe_write vector " - f"cannot be regenerated without them); nothing was written: {exc}", - file=sys.stderr, - ) - raise SystemExit(1) from exc # Error vectors, verified against the REAL implementation as we build them. - error_vectors = [ + built_errors = [ { "name": "truncated_frame", "frame_hex": "434b03", @@ -377,7 +430,7 @@ def generate() -> int: "error": "declared header length (255) exceeds the bytes present in the frame", }, ] - for vec in error_vectors: + for vec in built_errors: try: SerializationWrapper.unwrap(bytes.fromhex(vec["frame_hex"])) except ValueError: @@ -390,7 +443,7 @@ def generate() -> int: pass # exactly the trailing-bytes rejection the spec requires else: # pragma: no cover - generation-time invariant raise AssertionError("strict msgpack reader accepted a CK frame as one document") - error_vectors.append( + built_errors.append( { "name": "ck_frame_fed_to_interop_reader", "frame_hex": raw_frame.hex(), @@ -403,97 +456,25 @@ def generate() -> int: } ) - doc = { - "description": ( - "Python SDK (cachekit-py) auto-mode storage container: CK v3 frame. " - "Python-SDK-internal — other SDKs identify and reject, never decode. " - "See spec/wire-format.md 'SDK Storage Containers (auto mode)'." - ), - "frame_layout": "MAGIC 'CK' (0x43 0x4B) | VERSION u8 (0x03) | HDR_LEN u32 big-endian | HEADER (UTF-8 JSON: {s, m, v}) | PAYLOAD (raw bytes)", - "generator": f"cachekit {cachekit.__version__} (PyPI wheel; Rust core via PyO3), generated by tools/python-frame-reference.py generate", - "frame_vectors": vectors, - "error_vectors": error_vectors, - } - # `generate` rewrites the whole fixture, so it can only ever be additive by - # accident. Refuse to write if that rewrite would drop a vector that is - # already committed — the encoding sniff above catches a post-1.1 wheel, but - # a pre-1.1 wheel passes it and would silently delete the *_bin twins (and - # any other vector this tool no longer knows how to build). Both directions - # close here, on the names actually present, rather than on a wheel version. - dropped = {v["name"] for v in _load_fixture()["frame_vectors"]} - {v["name"] for v in vectors} - if dropped: - print( - f"REFUSED: regenerating would drop committed vector(s): {', '.join(sorted(dropped))}. " - "Committed vectors are the source of truth four SDKs pin — nothing written.", - file=sys.stderr, - ) - return 1 - VECTOR_PATH.write_text(json.dumps(doc, indent=2, sort_keys=False) + "\n") - print(f"wrote {VECTOR_PATH} ({len(vectors)} frame vectors, {len(error_vectors)} error vectors)") - return 0 - - -def generate_bin_twin() -> int: - """Append the protocol 1.1 bin-envelope twin of the default-path vector. - - Append-only (LAB-903): every existing vector is left byte-untouched — the - legacy vectors are the legacy-read proof and cannot be regenerated by a - post-0.4.0-core wheel. The twin gets its own generator provenance so the - fixture records which wheel produced which vector. - """ - import cachekit - - twin, encoding = _build_default_path_vector() - if encoding != "bin": - print( - f"installed cachekit {cachekit.__version__} still emits {encoding} envelopes; " - "generate-bin-twin needs a wheel carrying cachekit-core >= 0.4.0. Nothing written.", - file=sys.stderr, - ) - return 1 - - twin_name = "default_saas_write_msgpack_bytestorage_bin" - twin["name"] = twin_name - twin["description"] = ( - "Protocol 1.1 twin of default_saas_write_msgpack_bytestorage: same value, same " - "default @cache write path, but the ByteStorage envelope's compressed_data is " - "msgpack bin (serde_bytes) instead of an array of integers. Readers MUST accept " - "both encodings; the legacy encoding stays pinned by the legacy vector's bytes." - ) - twin["generator"] = ( - f"cachekit {cachekit.__version__} (PyPI wheel; Rust core via PyO3), generated by " - "tools/python-frame-reference.py generate-bin-twin" + # Upsert by name. The top-level 'generator' (the legacy-vector provenance) + # is never rewritten; every vector this run rewrites or adds carries its + # own per-vector 'generator' recording which wheel produced it. + generator_stamp = ( + f"cachekit {cachekit.__version__} (PyPI wheel; Rust core via PyO3), " + "generated by tools/python-frame-reference.py generate" ) + changed = _upsert(doc["frame_vectors"], built, generator_stamp) + changed += _upsert(doc["error_vectors"], built_errors, generator_stamp) + _require_twin_equivalence(doc["frame_vectors"]) - doc = _load_fixture() - by_name = {v["name"]: v for v in doc["frame_vectors"]} - if twin_name in by_name: - # Append-only and idempotent: a no-op re-run is success, not failure. - print(f"{twin_name} already present; nothing written.", file=sys.stderr) + if not changed: + print(f"{VECTOR_PATH} already up to date ({len(built)} frame, {len(built_errors)} error vectors rebuilt identical); nothing written") return 0 - - # The description above asserts the ENCODING is the only difference from the - # legacy vector. Prove it, rather than trusting the wheel: a wheel that also - # changed the LZ4 level, msgpack key order, or the frame header would append a - # vector that lies about what it isolates, into a fixture downstream SDKs pin. - legacy = by_name.get("default_saas_write_msgpack_bytestorage") - _require(legacy is not None, "legacy default-path vector missing — cannot prove the twin is a twin") - assert legacy is not None # narrowing for type checkers; _require already raised - _require(twin["value_json"] == legacy["value_json"], "twin value_json differs from the legacy vector") - _require(twin["expected_header"] == legacy["expected_header"], "twin frame header differs from the legacy vector") - for field in ("compressed_data_hex", "checksum_hex", "original_size", "format", "inner_msgpack_hex"): - _require( - twin["payload_envelope"][field] == legacy["payload_envelope"][field], - f"twin payload_envelope.{field} differs from the legacy vector — encoding must be the ONLY delta", - ) - doc["frame_vectors"].append(twin) - if not doc["generator"].startswith("legacy vectors:"): - doc["generator"] = ( - f"legacy vectors: {doc['generator']} (unchanged since); " - "*_bin twins carry their own per-vector 'generator' field" - ) VECTOR_PATH.write_text(json.dumps(doc, indent=2, sort_keys=False) + "\n") - print(f"appended {twin_name} to {VECTOR_PATH} (cachekit {cachekit.__version__})") + print( + f"wrote {VECTOR_PATH} ({changed} vector(s) rewritten or added; " + f"{len(doc['frame_vectors'])} frame, {len(doc['error_vectors'])} error vectors total)" + ) return 0 @@ -504,9 +485,7 @@ def generate_bin_twin() -> int: sys.exit(0) if mode == "generate": sys.exit(generate()) - if mode == "generate-bin-twin": - sys.exit(generate_bin_twin()) if mode == "verify": sys.exit(verify()) - print(f"unsupported mode: {mode!r}; expected 'verify', 'generate', or 'generate-bin-twin'", file=sys.stderr) + print(f"unsupported mode: {mode!r}; expected 'verify' or 'generate'", file=sys.stderr) sys.exit(2) From ad90a3fd21c07dd9a96d1cf46bea8b1b6a954377 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 07:17:51 +1000 Subject: [PATCH 2/5] fix(tools): apply LAB-1203 expert-panel findings to python-frame-reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panel (high stakes, 4 agents) found one CRIT and two MAJ, all applied: - CRIT: sharing decode_envelope silently WEAKENED verify — the old tag sniff required the 0x94 fixarray(4) marker, the shared decoder tolerates array16/array32 outer headers (reader-lenient), and the Node cross-check was always lenient there too, so a spec-violating dc0004 envelope passed both verifiers. verify now requires the envelope to re-encode byte-identically via the shared codec, pinning the canonical rmp_serde shortest-form encoding — strictly stronger than the pre-refactor check. - MAJ: the twin-equivalence proof compared expected_header as a parsed dict; a wheel changing header JSON serialization (dict-equal, byte-different) could upsert a byte-level non-twin. Now compares frame prefixes (magic/version/header) at the byte level. - MAJ: rewriting a previously-unstamped vector would falsify the top-level 'unchanged since' provenance claim. generate now flips the top-level field to an explicit mixed-provenance statement (idempotent) when that happens. - MIN: verify pins declared compressed_data_hex/checksum_hex/original_size/ format against the decoded envelope bytes (previously Node-only); generate prints WHICH vectors it rewrote; no-op message no longer reads as fixture totals; dead type-narrowing assert removed. Mutation suite extended to 8 classes — all fail the stdlib verifier; the twin guard refuses both field drift and header-byte drift with named invariants. No-op generate remains byte-identical (subset and full venvs), second runs idempotent, full local verify.yml suite green. --- CHANGELOG.md | 14 ++++++- tools/python-frame-reference.py | 69 ++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab8ef5..399e4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,8 +103,18 @@ All notable changes to the CacheKit Protocol Specification. The ByteStorage envelope codec is no longer reimplemented there: encode/decode come from `tools/wire-format-reference.py`, the one shared implementation of the encoding these fixtures pin. Rewritten vectors carry per-vector - `generator` provenance; `test-vectors/python-frame.json` is byte-unchanged by - this refactor, and a no-op `generate` never rewrites the file. + `generator` provenance (and the top-level provenance flips to an explicit + "mixed provenance" statement the first time a previously-unstamped vector is + rewritten); `test-vectors/python-frame.json` is byte-unchanged by this + refactor, and a no-op `generate` never rewrites the file. The stdlib `verify` + leg got strictly stronger (expert-panel findings): it now fully decodes each + `payload_envelope` via the shared codec (enforcing the protocol 1.1 flip + exclusions — checksum stays an array of 8 integers, format stays fixstr), + requires the envelope to re-encode byte-identically (pinning the canonical + rmp_serde shortest-form encoding, including the outer fixarray(4) marker), + and pins the declared `compressed_data_hex`/`checksum_hex`/`original_size`/ + `format` fields against the actual envelope bytes; the generate-time twin + proof now compares frame prefixes at the byte level, not as parsed JSON. - 7 legacy/`bin` vector pairs in `test-vectors/wire-format.json` (append-only; legacy vectors are retained forever as legacy-read proof; fixture diff --git a/tools/python-frame-reference.py b/tools/python-frame-reference.py index 4928769..7bddcb9 100644 --- a/tools/python-frame-reference.py +++ b/tools/python-frame-reference.py @@ -146,7 +146,7 @@ def verify() -> int: # from the 1.1 flip: checksum stays an array of 8 integers and # format stays a fixstr, in BOTH encodings. try: - *_, actual = _wire.decode_envelope(payload) + data, checksum, size, fmt, actual = _wire.decode_envelope(payload) except ValueError as e: print(f"FAIL {name}: envelope decode: {e}") vec_failed += 1 @@ -154,8 +154,29 @@ def verify() -> int: if actual != declared: print(f"FAIL {name}: compressed_data is {actual}, vector declares {declared}") vec_failed += 1 + elif _wire.encode_envelope(data, checksum, size, fmt, encoding=actual) != payload: + # decode_envelope tolerates reader-lenient forms no + # rmp_serde writer emits (array16/array32 outer header, + # non-shortest uints); re-encode byte-fidelity pins the + # canonical writer form, incl. the fixarray(4) marker. + print(f"FAIL {name}: envelope is not in canonical shortest-form encoding (re-encode differs)") + vec_failed += 1 else: - observed_encodings.add(actual) + drifted = [ + fname + for fname, got in ( + ("compressed_data_hex", data.hex()), + ("checksum_hex", checksum.hex()), + ("original_size", size), + ("format", fmt), + ) + if env.get(fname) != got + ] + if drifted: + print(f"FAIL {name}: payload_envelope field(s) disagree with the envelope bytes: {', '.join(drifted)}") + vec_failed += 1 + else: + observed_encodings.add(actual) det = vec.get("arrow_detection") if det: off = det["ipc_magic_offset"] @@ -286,19 +307,19 @@ def _build_default_path_vector() -> dict: } -def _upsert(committed: list[dict], built: list[dict], generator_stamp: str) -> int: +def _upsert(committed: list[dict], built: list[dict], generator_stamp: str) -> list[str]: """Replace committed vectors the wheel rebuilt (matched by name); append new names. Never removes anything: a vector this run did not rebuild stays exactly as committed, so dropping a committed vector is structurally impossible. A rebuilt vector whose content matches the committed one (ignoring its per-vector 'generator' provenance) keeps the committed entry byte-untouched - — a no-op `generate` leaves the fixture byte-identical. Returns the number - of vectors actually rewritten or added. + — a no-op `generate` leaves the fixture byte-identical. Returns the names + of the vectors actually rewritten or added. """ index = {v["name"]: i for i, v in enumerate(committed)} _require(len(index) == len(committed), "committed fixture has duplicate vector names") - changed = 0 + changed: list[str] = [] for vec in built: i = index.get(vec["name"]) if i is not None and {k: v for k, v in committed[i].items() if k != "generator"} == vec: @@ -309,7 +330,7 @@ def _upsert(committed: list[dict], built: list[dict], generator_stamp: str) -> i committed.append(stamped) else: committed[i] = stamped - changed += 1 + changed.append(vec["name"]) return changed @@ -323,12 +344,19 @@ def _require_twin_equivalence(frame_vectors: list[dict]) -> None: downstream SDKs pin (LAB-903). """ by_name = {v["name"]: v for v in frame_vectors} - legacy = by_name.get("default_saas_write_msgpack_bytestorage") - twin = by_name.get("default_saas_write_msgpack_bytestorage_bin") - _require(legacy is not None and twin is not None, "default-path vector pair incomplete") - assert legacy is not None and twin is not None # narrowing; _require already raised + try: + legacy = by_name["default_saas_write_msgpack_bytestorage"] + twin = by_name["default_saas_write_msgpack_bytestorage_bin"] + except KeyError as exc: + raise ValueError("generation invariant violated: default-path vector pair incomplete") from exc _require(twin["value_json"] == legacy["value_json"], "twin value_json differs from the legacy vector") - _require(twin["expected_header"] == legacy["expected_header"], "twin frame header differs from the legacy vector") + # Header equality must hold at the BYTE level, not just as parsed JSON — a + # wheel that reorders or reformats the header JSON would otherwise slip a + # byte-level non-twin past a dict compare. The frame prefix is everything + # before the payload: magic, version, header length, header bytes. + legacy_prefix = legacy["frame_hex"][: len(legacy["frame_hex"]) - len(legacy["expected_payload_hex"])] + twin_prefix = twin["frame_hex"][: len(twin["frame_hex"]) - len(twin["expected_payload_hex"])] + _require(twin_prefix == legacy_prefix, "twin frame prefix (magic/version/header bytes) differs from the legacy vector") for field in ("compressed_data_hex", "checksum_hex", "original_size", "format", "inner_msgpack_hex"): _require( twin["payload_envelope"][field] == legacy["payload_envelope"][field], @@ -463,17 +491,28 @@ def generate() -> int: f"cachekit {cachekit.__version__} (PyPI wheel; Rust core via PyO3), " "generated by tools/python-frame-reference.py generate" ) + unstamped = {v["name"] for v in doc["frame_vectors"] + doc["error_vectors"] if "generator" not in v} changed = _upsert(doc["frame_vectors"], built, generator_stamp) changed += _upsert(doc["error_vectors"], built_errors, generator_stamp) _require_twin_equivalence(doc["frame_vectors"]) if not changed: - print(f"{VECTOR_PATH} already up to date ({len(built)} frame, {len(built_errors)} error vectors rebuilt identical); nothing written") + print( + f"{VECTOR_PATH} already up to date ({len(built)} frame, {len(built_errors)} error " + "vectors rebuilt, all identical to committed); nothing written" + ) return 0 + if unstamped & set(changed) and not doc["generator"].startswith("mixed provenance"): + # Rewriting a vector the top-level provenance claim covered would turn + # that claim into a lie; per-vector 'generator' becomes authoritative. + doc["generator"] = ( + "mixed provenance — vectors carrying a per-vector 'generator' field record " + f"their own; all others: {doc['generator']}" + ) VECTOR_PATH.write_text(json.dumps(doc, indent=2, sort_keys=False) + "\n") print( - f"wrote {VECTOR_PATH} ({changed} vector(s) rewritten or added; " - f"{len(doc['frame_vectors'])} frame, {len(doc['error_vectors'])} error vectors total)" + f"wrote {VECTOR_PATH} — rewrote/added: {', '.join(changed)} " + f"({len(doc['frame_vectors'])} frame, {len(doc['error_vectors'])} error vectors total)" ) return 0 From 3a6cd521ea30911ce70e64cc525d2a747a768464 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Mon, 31 Aug 2026 08:10:31 +1000 Subject: [PATCH 3/5] fix(tools): tolerate a missing default-path twin in _require_twin_equivalence (LAB-1203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kody review finding on PR #56: generate() only ever rebuilds the one default-path vector the installed wheel's encoding selects, but the twin-equivalence guard hard-required BOTH twins in the merged fixture, so a partial fixture (fresh bootstrap, or a deliberately removed vector) aborted before writing anything — a regression from the removed append-only generate-bin-twin, which tolerated an absent twin. The guard now no-ops when either twin is absent (nothing is comparable) and prints a stderr note instead of skipping silently. Enforcement when both twins exist is unchanged — field drift and frame-prefix byte drift are still refused before any write. Completeness stays gated where it always was: verify()'s coverage floor fails the fixture until both int-array and bin encodings are observed, and _upsert never removes, so an established fixture cannot regress into the skip branch. Expert panel (high stakes, 4 agents) on this diff: bug-hunter, security and pragmatism legs all clear; craftsman's doc findings applied (docstring states the no-op, comment states the floor pins encodings not names). Rejected: pinning the twin names inside verify()'s coverage floor — that reintroduces the same fixture-shape rigidity in the verifier that this fix removes from the generator. --- tools/python-frame-reference.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tools/python-frame-reference.py b/tools/python-frame-reference.py index 7bddcb9..969f163 100644 --- a/tools/python-frame-reference.py +++ b/tools/python-frame-reference.py @@ -342,13 +342,22 @@ def _require_twin_equivalence(frame_vectors: list[dict]) -> None: also changed the LZ4 level, msgpack key order, or the frame header would upsert a vector that lies about what it isolates, into a fixture downstream SDKs pin (LAB-903). + + No-op when either default-path twin is absent (partial fixture): generate() + only ever rebuilds the encoding the wheel emits, so nothing is comparable + until both exist. Completeness is gated by verify(), not here. """ by_name = {v["name"]: v for v in frame_vectors} - try: - legacy = by_name["default_saas_write_msgpack_bytestorage"] - twin = by_name["default_saas_write_msgpack_bytestorage_bin"] - except KeyError as exc: - raise ValueError("generation invariant violated: default-path vector pair incomplete") from exc + legacy = by_name.get("default_saas_write_msgpack_bytestorage") + twin = by_name.get("default_saas_write_msgpack_bytestorage_bin") + if legacy is None or twin is None: + # Partial fixture (fresh bootstrap, or a deliberately removed vector). + # verify's coverage floor pins ENCODINGS, not these names — it fails + # the fixture until both int-array and bin are observed, which today + # only this pair carries. _upsert never removes, so an established + # fixture can never regress into this branch. + print("note: default-path twin pair incomplete; equivalence proof skipped", file=sys.stderr) + return _require(twin["value_json"] == legacy["value_json"], "twin value_json differs from the legacy vector") # Header equality must hold at the BYTE level, not just as parsed JSON — a # wheel that reorders or reformats the header JSON would otherwise slip a From e803fdaaa4cf84271456ea169c4133292ad12d4d Mon Sep 17 00:00:00 2001 From: mark-s Date: Mon, 31 Aug 2026 10:10:35 +1000 Subject: [PATCH 4/5] fix(tools): annotate the codec loader, split the error-vector flow (LAB-1203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CodeRabbit findings on PR #56: - ANN202: _load_wire_format_codec returns a module, so annotate it ModuleType rather than leaving the one unannotated def in the file. - PLR0915: generate() carried 56 statements against a 50 ceiling. The error-vector construction is the natural seam — it depends only on raw_frame plus the two generation-time imports and shares no state with the frame-vector flow — so it moves to _build_error_vectors(). Every real-implementation check moves with it unchanged: each vector is still proven rejected by cachekit-py, and the interop vector still proven rejected by a strict msgpack reader, before anything can be written. - CHANGELOG overstated the twin-equivalence guard. Since 3a6cd52 the guard no-ops when either twin is absent, so the entry now scopes the claim to a complete pair and names the skip. The extraction is proven behaviour-preserving, not merely assumed: running generate on the same wheel before and after produces byte-identical output (both report the fixture already up to date, nothing written), and the stdlib verify leg — the CI check — passes on all 8 vectors. verify()'s own PLR0915 (79 statements) is pre-existing and untouched here. --- CHANGELOG.md | 6 +- tools/python-frame-reference.py | 100 ++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399e4dd..020b890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,8 +98,10 @@ All notable changes to the CacheKit Protocol Specification. a committed vector is structurally impossible — which deletes the LAB-903 drop-refusal guard and both wheel-direction refusals, and folds the append-only `generate-bin-twin` mode into `generate` (a protocol 1.1 wheel - rebuilds the `_bin` twin, a legacy wheel the legacy original; the default-path - pair is still proven to differ only in envelope encoding before writing). + rebuilds the `_bin` twin, a legacy wheel the legacy original; whenever both + default-path vectors are present the pair is still proven to differ only in + envelope encoding before writing, and a partial fixture missing either twin + skips that proof with a stderr note rather than aborting). The ByteStorage envelope codec is no longer reimplemented there: encode/decode come from `tools/wire-format-reference.py`, the one shared implementation of the encoding these fixtures pin. Rewritten vectors carry per-vector diff --git a/tools/python-frame-reference.py b/tools/python-frame-reference.py index 969f163..8721855 100644 --- a/tools/python-frame-reference.py +++ b/tools/python-frame-reference.py @@ -46,6 +46,7 @@ import json import sys from pathlib import Path +from types import ModuleType VECTOR_PATH = Path(__file__).resolve().parent.parent / "test-vectors" / "python-frame.json" @@ -54,7 +55,7 @@ PREFIX_LEN = 7 # magic(2) + version(1) + header_len(4) -def _load_wire_format_codec(): +def _load_wire_format_codec() -> ModuleType: """Load tools/wire-format-reference.py as a module (hyphenated filename).""" path = Path(__file__).resolve().parent / "wire-format-reference.py" spec = importlib.util.spec_from_file_location("wire_format_reference", path) @@ -373,6 +374,59 @@ def _require_twin_equivalence(frame_vectors: list[dict]) -> None: ) +def _build_error_vectors(raw_frame: bytes, msgpack: ModuleType, wrapper: type) -> list[dict]: + """Build the error vectors, each checked against the REAL implementation. + + Split out of generate() so the frame-vector flow and the error-vector flow + read independently (PLR0915). Behaviour is unchanged: every vector here is + proven to be rejected by cachekit-py before it can be written, and the + interop vector is proven to be rejected by a strict msgpack reader. + """ + built_errors = [ + { + "name": "truncated_frame", + "frame_hex": "434b03", + "error": "shorter than the 7-byte fixed prefix (magic + version + header length)", + }, + { + "name": "unsupported_frame_version", + "frame_hex": "434b04000000027b7d", + "error": "frame version 4 (only version 3 is defined)", + }, + { + "name": "header_overrun", + "frame_hex": "434b03000000ff7b7d", + "error": "declared header length (255) exceeds the bytes present in the frame", + }, + ] + for vec in built_errors: + try: + wrapper.unwrap(bytes.fromhex(vec["frame_hex"])) + except ValueError: + pass + else: # pragma: no cover - generation-time invariant + raise AssertionError(f"cachekit-py accepted error vector {vec['name']}") + try: + msgpack.unpackb(raw_frame) + except msgpack.exceptions.ExtraData: + pass # exactly the trailing-bytes rejection the spec requires + else: # pragma: no cover - generation-time invariant + raise AssertionError("strict msgpack reader accepted a CK frame as one document") + built_errors.append( + { + "name": "ck_frame_fed_to_interop_reader", + "frame_hex": raw_frame.hex(), + "error": ( + "not a single well-formed MessagePack document: 0x43 is fixint 67, so the frame is one " + "1-byte document plus trailing bytes. Interop readers MUST consume exactly one document " + "and reject trailing bytes; on failure, a 0x43 0x4B prefix SHOULD be reported as " + "'Python-SDK-internal auto-mode entry — not an interop value'" + ), + } + ) + return built_errors + + def generate() -> int: import msgpack # third-party; generation only @@ -449,49 +503,7 @@ def generate() -> int: } ) - # Error vectors, verified against the REAL implementation as we build them. - built_errors = [ - { - "name": "truncated_frame", - "frame_hex": "434b03", - "error": "shorter than the 7-byte fixed prefix (magic + version + header length)", - }, - { - "name": "unsupported_frame_version", - "frame_hex": "434b04000000027b7d", - "error": "frame version 4 (only version 3 is defined)", - }, - { - "name": "header_overrun", - "frame_hex": "434b03000000ff7b7d", - "error": "declared header length (255) exceeds the bytes present in the frame", - }, - ] - for vec in built_errors: - try: - SerializationWrapper.unwrap(bytes.fromhex(vec["frame_hex"])) - except ValueError: - pass - else: # pragma: no cover - generation-time invariant - raise AssertionError(f"cachekit-py accepted error vector {vec['name']}") - try: - msgpack.unpackb(raw_frame) - except msgpack.exceptions.ExtraData: - pass # exactly the trailing-bytes rejection the spec requires - else: # pragma: no cover - generation-time invariant - raise AssertionError("strict msgpack reader accepted a CK frame as one document") - built_errors.append( - { - "name": "ck_frame_fed_to_interop_reader", - "frame_hex": raw_frame.hex(), - "error": ( - "not a single well-formed MessagePack document: 0x43 is fixint 67, so the frame is one " - "1-byte document plus trailing bytes. Interop readers MUST consume exactly one document " - "and reject trailing bytes; on failure, a 0x43 0x4B prefix SHOULD be reported as " - "'Python-SDK-internal auto-mode entry — not an interop value'" - ), - } - ) + built_errors = _build_error_vectors(raw_frame, msgpack, SerializationWrapper) # Upsert by name. The top-level 'generator' (the legacy-vector provenance) # is never rewritten; every vector this run rewrites or adds carries its From e7dfbdc7e77b02744f74b946e0c6312c4ec8fb90 Mon Sep 17 00:00:00 2001 From: mark-s Date: Mon, 31 Aug 2026 10:28:20 +1000 Subject: [PATCH 5/5] refactor(tools): drop the injected module/class from _build_error_vectors (LAB-1203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expert-panel finding (bug-hunter, security, craftsman, catchphrase; high stakes). Both quality reviewers converged independently: threading `msgpack` and `SerializationWrapper` in as parameters bought no seam at a single call site, and the annotations were vacuous — `type` admits every class and `ModuleType` every module, so neither constrained what the body actually needs. The file's own sibling `_build_default_path_vector()` already sets the convention: no parameters, function-local generation-only imports. Both are sys.modules-cached by the time this runs, so the re-import costs nothing. Also dropped the "(PLR0915)" citation from the docstring. This repo has no pyproject.toml and no ruff config, and CI runs only the vector verify — the rule came from a review bot's on-diff analysis, so naming it in a docstring pointed at a gate that does not exist and invited someone to "finish the job" on verify() (79 statements, deliberately out of scope here). Still byte-identical: generate on the same wheel reports the fixture already up to date, and the emitted file diffs clean against the committed one. Panel findings rejected, with reasons: - Craftsman: make the two moved invariants raise ValueError via the file's _require() convention rather than AssertionError. Both raises predate this PR and moved across unchanged; rewriting them would break the character-identical-lift property this extraction was accepted on, for a pre-existing asymmetry outside the diff. Worth its own ticket. - Catchphrase: trim the saas wildcard comment to two lines. Kept at five — the LAB-1207 memory entry recorded the buggy path==='/*' skip *as design*, which is precisely the repeat this comment exists to prevent. The craftsman independently judged it sufficient. --- tools/python-frame-reference.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tools/python-frame-reference.py b/tools/python-frame-reference.py index 8721855..8427da4 100644 --- a/tools/python-frame-reference.py +++ b/tools/python-frame-reference.py @@ -374,14 +374,18 @@ def _require_twin_equivalence(frame_vectors: list[dict]) -> None: ) -def _build_error_vectors(raw_frame: bytes, msgpack: ModuleType, wrapper: type) -> list[dict]: +def _build_error_vectors(raw_frame: bytes) -> list[dict]: """Build the error vectors, each checked against the REAL implementation. - Split out of generate() so the frame-vector flow and the error-vector flow - read independently (PLR0915). Behaviour is unchanged: every vector here is - proven to be rejected by cachekit-py before it can be written, and the - interop vector is proven to be rejected by a strict msgpack reader. + Separate from generate() so the frame-vector flow and the error-vector flow + read independently. Every vector here is proven to be rejected by + cachekit-py before it can be written, and the interop vector is proven to + be rejected by a strict msgpack reader. """ + import msgpack # third-party; generation only + + from cachekit.serializers.wrapper import SerializationWrapper + built_errors = [ { "name": "truncated_frame", @@ -401,7 +405,7 @@ def _build_error_vectors(raw_frame: bytes, msgpack: ModuleType, wrapper: type) - ] for vec in built_errors: try: - wrapper.unwrap(bytes.fromhex(vec["frame_hex"])) + SerializationWrapper.unwrap(bytes.fromhex(vec["frame_hex"])) except ValueError: pass else: # pragma: no cover - generation-time invariant @@ -503,7 +507,7 @@ def generate() -> int: } ) - built_errors = _build_error_vectors(raw_frame, msgpack, SerializationWrapper) + built_errors = _build_error_vectors(raw_frame) # Upsert by name. The top-level 'generator' (the legacy-vector provenance) # is never rewritten; every vector this run rewrites or adds carries its