M1: field_variable_kind (offset_map) now matches Record — a non-final
inline length-prefixed record field in aligned mode hits the ADR-006
rejection instead of computing silently corrupt offsets (probe-verified
clobber in the review: counts prefix at 0, id at 4).
M2: aligned record path locked with public-path tests —
materialize_aligned roundtrip (record<uint16>, wire arithmetic
asserted) and engine validate_bytes roundtrip + corrupted-buffer
rejection (record<uint32>). Record-as-last-field is the only safe
inline position post-M1.
M3: read_field's unreachable Struct arm (no struct-path entry ever
exists in an OffsetMap) replaced with a documented defensive Offset
error; doc comment states struct paths have no entry and the Offset
miss is the reachable composite failure. FieldValue::Struct (public
API, constructed by the packed reader) untouched.
Tests: 7 new (2 offset_map, 2 materialize, 1 engine M3 lock, plus the
roundtrip pair). 508 tests green, clippy -D warnings clean, wasm32
build green, cargo doc zero warnings.
Cyclic or over-deep $ref graphs stack-overflowed the three standalone
schema walkers (OffsetMap::compute, LayoutBuilder::new,
materialize_aligned) — SIGABRT on probe, parity-preserved from 0.2.0.
- New src/walk_guard.rs: check_ref_graph() — one bounded walk over the
reachable reference graph (depth cap 128 matching the plan compilers,
path-scoped cycle set; diamonds allowed, cycles and 201-def chains
rejected with the plan compilers' error wording)
- All three walkers run the guard at entry, before any recursion;
materialize_aligned's is defense-in-depth (a cyclic doc can no longer
produce an OffsetMap, but mismatched doc/map inputs must still fail
cleanly)
- Behavioral side effect, net-positive: the guard eagerly parses every
reachable def, so an invalid non-root def now surfaces at
LayoutBuilder::new instead of build() — four H3 tests updated to
expect the same Schema error earlier
- Test family: 12 new tests (walk_guard, offset_map, layout_builder,
materialize) covering self/two-def/composite-carrier cycles, deep
chains, and diamond non-rejection; no stack-overflow reproducers
in-tree per the review's Methodology warning
- Stale "walkers have no cycle guard" statements updated in
validation.md, 030 plan, ADR-012, and the engine gate comment
Verified: 501 tests green (423 + 17 + 34 + 15 + 12 + 2 ignored),
clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
Decision (recorded as an ADR-011 addendum): the packed-mode wire layout
for a field-name-discriminator TUnion is shared-then-variant — the
union's declared `fields` (disc + shared fields) first, then the
variant's own fields. Reader and materializer already implemented this;
LayoutBuilder was corrected from variant-only layout.
Enforcement in BastUnion::parse (the choke point every consumer
inherits — union roots at BastDoc::new, referenced unions at
resolve_ref):
- discriminator field must be declared in `fields`
- `fields` must not contain duplicate names
- variants must not re-declare shared fields (checked inline and
through $ref resolution — parse chain now threads the doc root)
- the discriminator field must be the FIRST entry in `fields` (the
reader reads the disc at the union start; a later position made it
dispatch on the wrong bytes — H3 item 2, probe-verified)
Schemas relying on the old variant-only builder convention (variants
re-declaring shared fields) are rejected with a clean Schema error
naming the convention. Breaking for 0.2.0-era re-declaring schemas;
announced with 0.3.x.
- L5: FieldValue::Union::variant_start doc now states per-kind
semantics (byte-disc: union_start + disc.offset + disc.size;
field-disc: after the shared walk).
- L6: roundtrip test added (poc_roundtrip.rs) — LayoutBuilder write →
SequentialReader read → materialize_packed → validate_bytes over a
field-disc union with a second shared field and non-redeclaring
variant; pins event.type@0/seq@1/handle@5, total 10.
- ADR-011: Status-block addendum recording the convention decision,
the no-re-declare rule, and the breaking-constraint note.
- Review #006 updated: H3/L5/L6 resolution blocks, resolution log,
recommended order.
Verified: 488 tests green (410+17+34+15+12, 2 pre-existing ignored),
clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
- Replace the three Vec::with_capacity(count) sites in materialize.rs
with Vec::new() — validate_bytes on an adversarial count no longer
OOM-aborts the process (AGENTS.md §3).
- New compile-time caps in schema.rs: MAX_ARRAY_ELEMENTS (2^16,
enforced at BastArray::parse — the choke point every consumer
inherits, bounds the walkers' per-element entry loops) and
MAX_ARRAY_BYTES (2^26, enforced per walker against the mode-specific
stride: compile_array, walk_array, compute_array_field).
- L1: fixed_composite_size/fixed_plan_size now return
Result<Option<usize>>; unwrap_or_default() gone, overflow is a clean
Schema error instead of silent stride-0.
- Zero-progress guard: stride-0 arrays whose elements consume 0 bytes
(legal empty-struct elements) now error in plan_walk_variable_array_
size and materialize_array_packed instead of looping count times.
- Tests: 8 new (parse/build/compile rejections, cap boundary,
short-buffer clean error) + array_count_large_u64_parses_on_64bit
rewritten to assert the new cap rejection. In-tree tests assert only
the safe (compile-time) half per review #006's Methodology warning.
- Review #006 updated: H1/L1 resolution blocks, new finding N2
(unbounded align annotations, found while re-deriving the cap
arithmetic), resolution log, recommended order.
Verified: 482 tests green (405+17+34+14+12, 2 pre-existing ignored),
clippy -D warnings clean, wasm32-unknown-unknown build green.