You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat: Python implementation + pydantic adapter
Full port of wire v0 and plan columnar@2 — row and columnar layouts, packed
string columns via stdlib zlib raw-deflate, the same decoder limits, the same
error codes. The test suite runs the exact spec/vectors files the TypeScript
reference runs: all golden vectors both plans, all invalid inputs by error
code, the stored-block packed decode, and every fingerprint vector reproduced
byte-for-byte from an independent canonical serializer.
hyperfly.pydantic.compile maps pydantic v2 models to the same IR the zod
adapter produces (Optional[T] becomes the nullable field flag; bounds from
annotated-types; Literal/StrEnum to enum), so a shared schema fingerprints
identically from either language.
Cross-language hardening that surfaced while porting: the scaled-decimal
mantissa is now pinned to sign(v)*floor(|v|*10^s + 0.5) in pure IEEE ops —
Math.round and Python's banker's round disagree on half values, which would
have made encoders in different languages pick different bytes. Spec updated;
vectors unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix(vectors): portable lone-surrogate marker
serde_json (correctly) refuses to parse a raw \ud800 escape because Rust
strings cannot hold lone surrogates. The vector now carries a {"$surrogate"}
marker: JS and Python revive it, implementations whose strings cannot
represent one skip it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix: address codex retro-review findings across all three implementations
Conformance and safety fixes surfaced by an adversarial gate review, plus the
Rust core (which is now conformant with them from the start):
Cross-cutting (spec + TS + Python + Rust, with a new golden vector):
- Empty nested struct now round-trips. Columnar flattening created nested
containers lazily, so a row like {p:{}} with an all-optional nested struct
decoded to {} and then failed to re-encode. Containers are now materialized
at each leaf's declared position — order-preserving (Rust's ordered decode
caught a key-order regression the JS/Python order-insensitive asserts had
masked) and empty-safe.
- IR strings (literals, enum members, field names) with lone surrogates are
rejected with a typed error instead of silently fingerprinting a replacement
char (TS) or raising an untyped UnicodeEncodeError (Python).
- Columnar decode enforces max_depth at row-equivalent nesting, matching the
row plan.
- The scaled-decimal mantissa rule now forbids fused multiply-add contraction
in the spec, so the two IEEE operations round identically everywhere.
Python decoder safety:
- Packed inflate is bounded (declared-size cap, no zlib "0 = unlimited" hole)
and rejects truncated deflate streams via an explicit eof check.
- max_items is enforced on fixed-length arrays before allocation.
Adapter parity (the two adapters are now pinned to one identical artifact
string in cross-adapter tests):
- pydantic emits `optional` for fields with defaults, intersects multiple
integer constraints instead of overwriting, rejects extra="allow" models and
RootModel.
- zod folds exclusiveMinimum/exclusiveMaximum into integer bounds, so gt/lt
match pydantic's inclusive conversion.
Also lands the Rust core (hyperfly-core) passing every golden vector both
plans, and adds a Python version matrix + Rust job to CI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix: second codex gate round — deep conformance and portability hardening
Portability by construction:
- Field names that are array indices ("0", "10") or "__proto__" are rejected;
JS reorders integer-index object keys and traps __proto__ assignment, so these
could never round-trip or fingerprint identically across TS/Python/Rust. This
also lets the columnar decoder drop an earlier null-prototype workaround.
- The zod adapter rejects array-index enum members: zod itself discards their
declaration order (e.options returns them re-sorted), so it cannot honour the
IR's ordered member array. It also rejects lone surrogates with a typed
UnsupportedSchemaError carrying a path, instead of deferring to a generic error.
- nullable(literal null) is rejected: it gave one value two wire encodings.
- Rust constrains fixed-array length to the safe-integer domain, matching TS/Python.
Decoder correctness and safety, verified across all three with new shared vectors:
- Columnar depth is enforced only for participating leaves, matching the row
plan (an absent optional nested leaf no longer over-rejects); the same check
now guards columnar encoding.
- Fixed-length arrays enforce maxItems in TS and Rust (previously Python only).
- TS TextDecoder preserves a leading U+FEFF (ignoreBOM) as Python/Rust do.
- Packed inflaters require exactly one complete DEFLATE stream: bounded output,
truncation rejected, and trailing bytes after the final block rejected — via
unused_data (Python), full-consumption check (Rust), and a tight-stream probe
(TS, working around node's sync inflater silently ignoring trailing bytes).
A valid empty-total packed column decodes everywhere.
Adapter fidelity:
- pydantic maps Optional[T] to a nullable field and never to wire-optional
(defaults are always materialized, so exclude_unset dropped default_factory
values); supports annotated_types.Interval; routes field names and dumps
through serialization aliases so aliased models round-trip under validation.
- Both adapters are pinned to one identical artifact string in cross-adapter tests.
Spec: the scaled-decimal mantissa forbids FMA contraction; float column mode
ties resolve to the lowest mode byte; packing and inflater-strictness are
documented as capability-scoped. Hardening: codec deep-clones its IR at compile,
header magic no longer routes through the mutable exported constant, and struct
encoding snapshots each field once so accessors cannot desync the bitmap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix: third codex gate round — prototype safety, inflate flags, depth semantics
The suite was genuinely red and I had reported it green: turbo served a cached
result and the check grepped that cached output. Verified directly from here on.
Blocking fixes:
- Rust could not decode its own packed columns. inflate_exact passed flags = 0,
omitting TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF, so anything larger than a
stored block failed. The golden vectors only carried tiny stored blocks, so a
self-roundtrip test now covers what cross-implementation vectors cannot.
- TS read struct fields through the prototype chain, so a field legitimately
named constructor or toString read an inherited value on encode and wrote onto
Object.prototype on decode. Encoding now snapshots own properties only, and
columnar container resolution uses hasOwnProperty.
- The property generators emitted nullable(literal null) schemas, which the
previous round made invalid.
Conformance:
- Columnar depth now mirrors row-plan structure exactly: a nested container at
chain position j sits at depth+2+j and is checked whenever rows exist, while
the leaf value at depth+1+len(segs) is checked only when it participates.
Previously an absent optional leaf let an over-deep nested struct through.
- A nullable flag over a null literal is rejected, closing the field-level
variant of an ambiguity fixed at node level last round.
- Empty columns must carry mode 0x00; a nonzero mode with no payload was
accepted and re-encoded differently.
- Python's array-index field-name check is length-bounded before int(), which
raises on 3.11+ for very long digit strings.
- pydantic rejects split validation/serialization alias pairs instead of
emitting an IR name that model_dump never produces.
- Codecs deep-copy their IR in Python as they already did in TS.
spec/wire-v0.md now documents the portable-field-name, well-formed-Unicode and
unambiguous-null rules, so an implementation following the spec alone accepts
exactly what these three accept.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix: fourth codex gate round — no blockers, four conformance gaps closed
- Empty int and float columns now require mode 0x00 on decode in TypeScript;
only the string column had the guard, so TS accepted a nonzero mode with no
payload that Python and Rust rejected.
- Compiled IR is deep-frozen in TypeScript and exposed as a copy in Python, so
codec.ir can no longer drift from the fingerprint that names it.
- Python columnar float encoding survives the full float64 domain: probing a
decimal scale for values near float_info.max overflowed to infinity and
raised OverflowError instead of falling back to raw mode.
- The pydantic adapter compares the serialization name against the validation
name and rejects any field where they differ, including one-sided aliases
that previously produced an IR name model_dump never emits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
* fix: fifth codex gate round — allocation bound, symmetric packing, encoder limits
Blocking: a declared array count is now bounded by the bytes still on the wire.
Any element carrying payload costs at least one bit, so four malformed bytes
claiming 2^24 rows no longer make TS, Python, or Rust allocate sixteen million
rows before noticing the body is empty. maxItems remains the ceiling; this is
the floor that makes truncation cheap to reject.
Also:
- A codec that cannot inflate no longer emits packed string columns. Asymmetric
hooks let a codec write bytes its own decoder refused as unsupported.
- Encoders enforce their codec's maxByteLength and maxItems, so an encoder can
no longer produce output that the same codec rejects on the way back in.
- The pydantic adapter rejects AliasChoices and AliasPath validation aliases;
the previous check only compared strings and let those through with a wire
name validation would never accept. Its wrapper also exposes ir as a copy,
matching the core codec.
- The compiled codec object is frozen in TS, so codec.ir cannot be reassigned
to something the fingerprint does not describe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_hf1
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0 commit comments