diff --git a/docs/reviews/007-coverage-audit.md b/docs/reviews/007-coverage-audit.md
new file mode 100644
index 0000000..208955a
--- /dev/null
+++ b/docs/reviews/007-coverage-audit.md
@@ -0,0 +1,400 @@
+---
+status: resolved (F1, F2, C1, C2, C3, L1, L2 resolved 2026-09-02; N1/N2a/N3a/N4a are classified-no-action / deferred-by-design)
+last_updated: 2026-09-02
+reviewed_artifacts:
+ - src/{materialize,sequential_reader,read_plan,offset_map,layout_builder,engine,data_access,bast,builder,tunion,validation_plan,walk_guard,bast_meta}.rs
+ - tests/{poc_roundtrip,tunion_dispatch,error_paths,engine_integration}.rs
+ - docs/reviews/006-implementation-review-030.md (post-fix coverage re-check)
+tool: cargo-llvm-cov 0.8.4 (--release, per-line text) + manual classification of every uncovered production line + disposable probe tests (run in-session, then deleted)
+reviewer: post-review-#006 coverage audit (session request: "check test coverage for weak spots, meaningful tests, non-happy-path posture")
+---
+
+# Review #007 — Post-#006 Coverage Audit
+
+## Purpose
+
+Review #006 closed every finding and its M4 coverage map, but the
+session-level posture (M4's item: "fold a coverage check into each fix
+session") had never been run as a *whole-tree* pass after all those
+fixes landed. This audit re-measures coverage after the eleven #006
+commits, reads every uncovered production line, and classifies it —
+the same "untested-but-fine / load-bearing / unreachable" discipline
+M4's map used. Two probes were run in disposable tests (deleted after
+the session, per #006's no-reproducer rule; neither was a crash
+hazard — both reproduce cleanly inside the default harness).
+
+## Methodology
+
+- `cargo llvm-cov --release` (0.8.4, same tool as #006): summary +
+ per-line text. TOTAL **90.67% lines / 86.32% functions** — stable
+ with #006's post-M4 numbers (90.60%), the expected drift after the
+ N3 fix sessions added parse gates + tests.
+- Per-file (worst first): `materialize.rs` 85.72, `data_access.rs`
+ 80.32, `sequential_reader.rs` 86.19, `bast.rs` 87.41,
+ `builder.rs` 91.28, `layout_builder.rs` 91.42, `tunion.rs` 92.02,
+ `offset_map.rs` 92.93, `read_plan.rs` 90.69, `engine.rs` 96.44,
+ `validation_plan.rs` 93.97, `walk_guard.rs` 98.04,
+ `bast_meta.rs` 98.92, `bast_validation.rs`/`error.rs`/`schema.rs`/
+ `macros.rs`/`validation.rs` 100.
+- Every uncovered line *outside* `#[cfg(test)]` modules (806 raw
+ lines) was read and classified. Lines inside test modules (the
+ `panic!("expected X, got {other:?}")` helpers) were excluded — they
+ distort per-file numbers (e.g. `bast.rs`'s 87.41% is really ~96%
+ production once its 60 helper lines are excluded).
+- Two suspicions were probe-verified with disposable tests:
+ the F1 cross-consumer divergence and the F2 unbounded-`maxLength`
+ compile. Probe transcripts quoted verbatim in the findings.
+- Happy-path posture audit: cross-checked which *error arms* adjacent
+ to covered code are 0-execution, and which public surfaces have only
+ success-path tests.
+
+## Baseline
+
+Audited at `main` HEAD `bb28ba3` ("Resolve N3"), 0.3.0, working tree
+clean. Full suite green (548 tests static + 2 ignored doctests, per
+#006's bookkeeping).
+
+## Summary Statistics
+
+| Severity | Count | Status |
+|----------|------:|--------|
+| High | 2 (F1, F2) | both resolved 2026-09-02 |
+| Medium | 3 (C1, C2, C3) | all resolved 2026-09-02 |
+| Low | 2 (L1, L2) | all resolved 2026-09-02 |
+| Info | 4 (N1, N2a, N3a, N4a) | classified: N1 artifact, N2a/N4a no-action, N3a deferred to pre-release review |
+
+**Resolution log:**
+
+- **F1 + F2 (2026-09-02):** resolved in one commit — see the
+ resolution blocks on each finding. 477 lib tests green (511
+ static + 2 ignored doctests across all targets), clippy
+ `-D warnings` clean, wasm build green.
+
+---
+
+## Findings
+
+### F1. Zero-progress guard missing in the plan materializer — `validate_bytes` accepts what `SequentialReader` rejects (cross-consumer divergence)
+
+**Files**: `src/materialize.rs:227-253` (`materialize_plan_array` — no
+guard), contrast `src/sequential_reader.rs:772-795`
+(`plan_walk_variable_array_size` — has the guard) and
+`src/materialize.rs:636-663` (`materialize_array_packed` — has the
+guard)
+
+**Problem**: The H1 fix session added the zero-progress runtime guard
+("array element consumed 0 bytes") to two of the three array walkers:
+the compiled reader's variable-array size walk and the legacy BAST
+walker's packed array arm. The *plan-based* packed materializer —
+`materialize_plan_array`, the walker `validate_bytes` actually uses in
+packed mode (engine.rs:310-312) — got no guard.
+
+A stride-0 array whose elements consume 0 bytes (empty-struct elements
+are legal: the meta-schema's `StructDef` has no `minItems` on
+`fields`) compiles with `element_stride: 0` and loops `count` times
+materializing empty objects without reading a single buffer byte:
+
+```
+PROBE validate_bytes([]): OK — zero-progress guard MISSING in plan materializer
+PROBE reader.read_next([]): Err(access error at items[0]: array element 0 consumed 0 bytes;
+ a zero-size element makes the declared count unbounded on the wire)
+```
+
+Schema: `{ "items": { "kind": "array", "element": { "kind":
+"struct", "fields": [] }, "count": 8 } }`, packed mode, empty buffer.
+Same schema, same buffer, opposite verdicts — the exact
+cross-consumer-disagreement shape review #006 existed for (H3, M6).
+Severity High by #006's own keying (AGENTS.md §3): `validate_bytes`
+is the flagship untrusted-input path, and it silently accepts a
+buffer the same engine's reader rejects. The H1 resolution text
+("plan_walk_variable_array_size (reader) and materialize_array_packed
+(materializer) now error") lists only two of the three walkers — the
+plan materializer was missed because it is *not* the legacy walker
+that finding named.
+
+**Not a #006 regression**: the H1 fix text itself specified only the
+reader and legacy-walker sites; the plan materializer predates the
+guard and was outside that fix's blast radius. But the divergence is
+new information — the guard's *invariant* ("a zero-progress element
+makes the declared count unbounded") belongs to the array-walk
+concept, not to two specific functions.
+
+**Fix**: hoist the same guard into `materialize_plan_array`'s loop
+(compare `*offset` before/after `materialize_plan_composite`; error
+with the same wording the other two walkers use so downstream
+matching sees one shape). Add a locking test driving the same schema
+through BOTH paths asserting the verdicts agree (both reject an empty
+buffer; both accept a buffer where the elements make progress —
+empty-struct elements never do, so the acceptance half needs a
+non-empty variant struct alongside).
+
+**Resolution (2026-09-02):** the guard, hoisted verbatim from the two
+existing sites (`*offset == before` after the element walk, same
+"array element {i} consumed 0 bytes…" wording so downstream matching
+sees one shape). Tests (3, in `materialize.rs`):
+`f1_zero_progress_array_rejected_by_all_three_walkers` (plan
+materializer + the record-value fallback path, both asserting the
+`Access` error with the guard's wording),
+`f1_validate_bytes_and_reader_agree_on_zero_progress_array` (the
+cross-consumer agreement the probe showed was missing —
+`validate_bytes` and `SequentialReader::read_next` both reject the
+same schema+buffer with the same error class),
+`f1_nonempty_variant_struct_array_still_materializes` (the
+false-positive check: elements that consume bytes still walk).
+Verified: 477 lib tests green, clippy `-D warnings` clean, wasm build
+green.
+
+### F2. `maxLength` is unbounded — the N2 analog
+
+**Files**: `src/bast_meta.rs:81` (`"maxLength": { "type": "integer",
+"minimum": 0 }` — no maximum), `src/bast.rs:1038-1043`
+(`parse_max_length` — no cap, and silently drops non-`usize` values),
+contrast `src/schema.rs` `MAX_ALIGN`/`parse_align` (the N2 pattern)
+
+**Problem**: N2 bounded `align` at 4096 with a clean parse error plus
+a meta-schema `"maximum"`. `maxLength` has the identical shape and
+was not covered by that fix:
+
+```
+PROBE aligned maxLength 1e12 compiles; total_size = 1099511627776
+```
+
+A one-field schema declares a 1 TiB reservation; `total_size` in that
+range is meaningless output the consumer may act on (N2's argument
+(a)). Unlike align, no `Access` error follows at read time (an empty
+buffer still fails buffer bounds first), so this is layout-meaningless
+output, not a crash — the exact severity N2 recorded. Additionally,
+`parse_max_length` returns `Option` and silently *drops* values that
+overflow `usize` (`.and_then(|n| usize::try_from(n).ok())`) — on a
+32-bit target a 5 GiB `maxLength` becomes "no maxLength", changing
+layout semantics without telling the consumer.
+
+**Fix**: the N2 playbook verbatim. A `MAX_LENGTH` cap in
+`schema.rs` (value TBD — `align`'s 4096 is page granularity; a
+reservation cap in the tens-of-megabytes range fits honest layouts;
+suggest `2^26 = 67_108_864`, matching `MAX_ARRAY_BYTES`'s rationale),
+enforced in `parse_max_length` (converted to `Result