Summary
apr export on any APR file that does not carry num_layers in its metadata panics at crates/aprender-core/src/format/converter/metadata.rs:384. APR-native files where num_layers was inferred at runtime (rather than stamped into metadata) cannot round-trip to GGUF.
Reproducer
$ apr export /home/noah/models/qwen2.5-coder-1.5b-instruct-q4k.apr --format gguf -o /tmp/rt.gguf
[PMAT-252] Raw passthrough: detected Q4K in APR source. Copying blocks directly (zero loss).
thread 'main' panicked at crates/aprender-core/src/format/converter/metadata.rs:384:10:
C-07: num_layers required for GGUF export
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
Evidence the model is actually valid
apr inspect --json returns 339 tensors and architecture: qwen2 but does not list num_layers or qwen2.block_count in metadata:
$ apr inspect --json /home/noah/models/qwen2.5-coder-1.5b-instruct-q4k.apr | jq '.architecture, .tensor_count, .metadata."qwen2.block_count"'
"qwen2"
339
null
Meanwhile apr trace --json enumerates 28 layers from the tensor names (blk.0.attn_q ... blk.27.ffn_down). The information is present in the file; the exporter just doesn't infer it.
Root cause
metadata.rs:382-384:
let num_layers = apr_metadata
.num_layers
.expect("C-07: num_layers required for GGUF export");
.expect() on an Option<usize> produces a panic (Rust exit 101), not a clean CliError. Per CLAUDE.md "unwrap() banned via .clippy.toml disallowed-methods. Use expect() or ok_or_else(|| ...)?" — expect is allowed but only as a sentinel for invariants that genuinely can't be violated. Here the invariant is violated (1.5B APR file in the model registry), so the call site is wrong.
num_heads and hidden_size directly below have the same shape and will panic identically for any APR file that omits them.
Suggested fix
- Infer
num_layers from tensor names when metadata is silent. The tensor count over blk.N.* already drives apr trace --json; reuse that logic in the GGUF exporter.
- Replace
.expect() with ok_or_else(|| AprenderError::FormatError { ... })? so a missing field surfaces as a clean validation error (exit 4), not a panic (exit 101).
- Backfill
num_layers/num_heads/hidden_size into the APR metadata at apr stamp time for forward-compatibility, and add a falsification test that round-trips APR → GGUF → APR for every architecture-family fixture in the registry.
Why it matters for v0.35.0
The README and SPEC-HF-PUBLISH-001 both list apr export --format gguf as part of the canonical publish pipeline. paiml/albor-370m-v1 shipped successfully because its specific stamping flow happened to populate num_layers. Users converting APR files produced by older stamp tooling, or by community workflows, will hit the panic and have no recovery path.
Severity
P1 — release-blocker for v0.35.0. Panic-on-valid-input from a load-bearing publish command. Filed alongside #1864 during v0.35.0 release dogfood.
Artifacts
- Host: noah-Lambda-Vector
- Model: /home/noah/models/qwen2.5-coder-1.5b-instruct-q4k.apr (339 tensors, 28 layers, qwen2)
- Build: HEAD = 0d8d52b (release/v0.35.0 worktree)
Summary
apr exporton any APR file that does not carrynum_layersin its metadata panics atcrates/aprender-core/src/format/converter/metadata.rs:384. APR-native files wherenum_layerswas inferred at runtime (rather than stamped into metadata) cannot round-trip to GGUF.Reproducer
Evidence the model is actually valid
apr inspect --jsonreturns 339 tensors andarchitecture: qwen2but does not listnum_layersorqwen2.block_countin metadata:Meanwhile
apr trace --jsonenumerates 28 layers from the tensor names (blk.0.attn_q...blk.27.ffn_down). The information is present in the file; the exporter just doesn't infer it.Root cause
metadata.rs:382-384:.expect()on anOption<usize>produces a panic (Rust exit 101), not a cleanCliError. Per CLAUDE.md "unwrap() banned via .clippy.toml disallowed-methods. Use expect() or ok_or_else(|| ...)?" —expectis allowed but only as a sentinel for invariants that genuinely can't be violated. Here the invariant is violated (1.5B APR file in the model registry), so the call site is wrong.num_headsandhidden_sizedirectly below have the same shape and will panic identically for any APR file that omits them.Suggested fix
num_layersfrom tensor names when metadata is silent. The tensor count overblk.N.*already drivesapr trace --json; reuse that logic in the GGUF exporter..expect()withok_or_else(|| AprenderError::FormatError { ... })?so a missing field surfaces as a clean validation error (exit 4), not a panic (exit 101).num_layers/num_heads/hidden_sizeinto the APR metadata atapr stamptime for forward-compatibility, and add a falsification test that round-trips APR → GGUF → APR for every architecture-family fixture in the registry.Why it matters for v0.35.0
The README and SPEC-HF-PUBLISH-001 both list
apr export --format ggufas part of the canonical publish pipeline.paiml/albor-370m-v1shipped successfully because its specific stamping flow happened to populatenum_layers. Users converting APR files produced by older stamp tooling, or by community workflows, will hit the panic and have no recovery path.Severity
P1 — release-blocker for v0.35.0. Panic-on-valid-input from a load-bearing publish command. Filed alongside #1864 during v0.35.0 release dogfood.
Artifacts