- Add README.md reflecting the v0.1.0 state: 19 AlkType kinds, two layout modes, builder + AlkTypeEngine usage example (verified to compile and run), validation entry points, crate independence, untrusted-schemas guarantee, docs pointers. Mirrors the alkvault README structure. - Add AGENTS.md with alktype-specific git workflow, project conventions (no comments, AlkTypeError, untrusted schemas, overflow safety, no async, no feature flags, wasm-clean, preserve_order load-bearing, no unsafe), verification commands, and ADR/OQ index. Blocks auto-commit on semver-relevant public API changes per the crates.io 0.1.0 contract. - Add LICENSE-MIT and LICENSE-APACHE (dual MIT/Apache-2.0, matching alkvault and the Cargo.toml license field). - Cargo.toml: add readme, keywords, categories, rust-version = "1.85". - Fix broken intra-doc link in builder.rs: DiscriminatorKind -> crate::schema::DiscriminatorKind (cargo doc now warning-free). - N1 (review #002): document is_rfc3339_timestamp as non-strict in the function doc comment. Lists the specific gaps (day-of-month per month, seconds range, leap seconds) and points consumers needing strict validation to chrono/time. - N2 (review #002): document the FieldValue::Bytes-for-Record API asymmetry in the FieldValue enum doc and on read_record_value. - .opencode/agents/implementation-specialist.md: point to AGENTS.md for full convention details (matches the alkvault pattern). - review #002: mark N1/N2 resolved; all 7 findings now closed. Verification: - cargo test --release: 396 tests pass (310 crate + 86 integration) - cargo clippy --all-targets -- -D warnings: clean - cargo doc --no-deps: clean (no broken intra-doc link warnings) - cargo build --target wasm32-unknown-unknown --release: clean - cargo publish --dry-run --allow-dirty: clean
23 KiB
status, last_updated, reviewed_artifacts, tool, reviewer
| status | last_updated | reviewed_artifacts | tool | reviewer | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| resolved (M1, M2, L1, L2, L3, N1, N2) | 2026-08-11 |
|
manual source read + cargo test/clippy/wasm build | pre-publish code review (pre-cargo-publish sweep) |
Code Review #002 — Pre-Publish Sweep
Purpose
First logic/correctness review of the alktype crate ahead of the
v0.1.0 crates.io publish. This pass complements the coverage review
(#001) by reading every source file for correctness, code smell, panic
safety, and API ergonomics — the things a downstream consumer
(notably the upcoming alkcall crate, the alknet-call/alknet-channels
unification) will trip over.
This review deliberately defers documentation polish (README, inline doc cleanup for docs.rs) and coverage gaps to subsequent sweeps, per the publisher's stated workflow. The scope here is: no panics in production paths, no silent data corruption, no API that returns nonsense, and nothing that would embarrass the crate on docs.rs on day one.
Methodology
- Full read of all 13
src/*.rsfiles (production + test modules). - Grep for
unwrap/expect/panic!/unreachable!/unsafe/TODO/FIXMEto classify every hit as production or test-only. - Grep for
as u32/as usize/as i32/as i64/as u64to find truncation-prone casts. cargo build --release,cargo test --release,cargo clippy --all-targets -- -D warnings,cargo build --target wasm32-unknown-unknown --release.- Cross-reference every error path against its caller to confirm errors propagate (not swallowed) and carry useful attribution.
- Read
docs/reviews/001-coverage-analysis.mdfor prior context and resolved/unresolved items.
Summary Statistics
| Severity | Count |
|---|---|
| Critical | 0 |
| Medium | 2 (M1, M2) |
| Low | 3 (L1, L2, L3) |
| Nit | 2 (N1, N2) |
No critical findings. The crate is in good shape for a 0.1.0 publish once M1 and M2 are resolved. The Medium findings are both small, localized, and have clear fixes; the Low/Nit findings are deferrable.
Verification Baseline
All verification run on the reviewed tree (commit c0217d9):
cargo test --release: 438 tests pass (391 crate unit tests + 47 integration tests across 4 files). Zero failures.cargo clippy --all-targets -- -D warnings: clean.cargo build --release: clean.cargo build --target wasm32-unknown-unknown --release: clean. The wasm32 target is a first-class supported target (the alkcall consumer will rely on this — confirmed working).- No
unsafeanywhere in the crate. - No
TODO/FIXME/HACK/XXXmarkers in source. - All
unwrap/expect/panic!/unreachable!are confined to#[cfg(test)]modules, verified by line-context cross-reference. Production code uses?,ok_or_else,checked_add, andResult-returning helpers throughout.
Findings
M1. Definitions::merge_into is convoluted and silently drops data
File: src/builder.rs:493-503
Problem: Definitions::merge_into is the public convenience API
for merging $defs into a top-level schema. The implementation goes
through self.build() (which returns {"$defs": {...}}), then
extracts the inner $defs object via a double-cloned/double-
unwrap_or_default chain:
pub fn merge_into(self, top: &mut Value) {
if let Some(obj) = top.as_object_mut() {
if let Some(defs) = self.build().as_object() {
if let Some(existing) = obj.get_mut("$defs").and_then(Value::as_object_mut) {
existing.extend(defs.get("$defs").cloned().unwrap_or_default()
.as_object().cloned().unwrap_or_default());
} else {
obj.insert("$defs".to_string(), Value::Object(
defs.get("$defs").cloned().unwrap_or_default()
.as_object().cloned().unwrap_or_default()));
}
}
}
}
Two issues:
-
Silent data loss: if
self.build()ever produces a shape other than{"$defs": Object(_)}(a future bug, or a refactor that adds another key), the.unwrap_or_default()chain silently substitutes an empty map — the definitions vanish without an error. This is the worst failure mode for a builder API: the consumer thinks they merged definitions, the schema compiles, and$refresolution silently fails downstream. -
Readability: the double-
cloned().unwrap_or_default().as_object() .cloned().unwrap_or_default()chain is hard to read and hard to audit. A reviewer cannot tell at a glance whether it's correct.
Also: merge_into has no test coverage. The
builder_definitions_define_returns_ref test covers
Definitions::build() but not merge_into. The OQ-006 resolution
work referenced merge_into in builder.md Example 3, so the API is
intended to be used — but it's never exercised.
Fix: Simplify by accessing the defs field directly (the method
already takes self by value) instead of round-tripping through
build(). Then add tests covering both the "merge into schema with
existing $defs" and "merge into schema without $defs" branches.
pub fn merge_into(self, top: &mut Value) {
if let Some(obj) = top.as_object_mut() {
match obj.get_mut("$defs").and_then(Value::as_object_mut) {
Some(existing) => existing.extend(self.defs),
None => {
obj.insert("$defs".to_string(), Value::Object(self.defs));
}
}
}
}
This requires moving the method outside the impl Definitions block
that consumes self.defs via build(), or restructuring so merge_into
takes ownership of self.defs directly. Either way, the
unwrap_or_default chain disappears.
Lift: removes a silent-data-loss bug class, makes the public API auditable, adds test coverage to an untested public method. Small effort (~15 lines + ~15 lines of tests).
M2. write_bytes silently truncates length prefix on >4 GiB data
File: src/data_access.rs:264-278
Problem: write_bytes casts data_len: usize to u32 for the
length prefix without checking bounds:
let data_len = value.len();
let total = U32_SIZE.checked_add(data_len).ok_or_else(/* overflow */)?;
let end = offset.checked_add(total).ok_or_else(/* overflow */)?;
check_bounds(buffer.len(), offset, end, field_path)?;
write_array(buffer, offset, u32_to(data_len as u32, endian), field_path)?;
If value.len() > u32::MAX (on a 64-bit platform with a >4 GiB blob),
data_len as u32 truncates. The check_bounds call above passes
(the buffer genuinely is that large, or total overflowed usize
first on a 32-bit target), but the written length prefix doesn't
match the actual data length. A subsequent read_bytes returns a
short slice — silent data corruption.
The checked_add guards on total and end catch usize overflow
but not the u32 truncation, which can happen before usize
overflow on 64-bit platforms (where usize is 64-bit and u32::MAX < usize::MAX).
The same as u32 cast pattern appears in:
src/tunion.rs:457,493— test-only, fine.src/sequential_reader.rs:860— test-only helper, fine.src/engine.rs:857— test-only, fine.
Only data_access::write_bytes is the public API path.
Fix: validate the cast before writing:
let data_len_u32 = u32::try_from(data_len).map_err(|_| {
access_err(
field_path,
format!("data length {data_len} exceeds u32::MAX (length prefix width)"),
)
})?;
write_array(buffer, offset, u32_to(data_len_u32, endian), field_path)?;
Lift: closes a silent-corruption path on large inputs. ~3 lines.
The 64-bit >4 GiB case is rare but not impossible (the crate handles
binary blobs via AlkType:Bytes; a safetensors-style data format
could plausibly have large blobs). For a 0.1.0 publish, closing this
path is worth the 3 lines.
L1. materialize.rs union path uses unchecked offset arithmetic
File: src/materialize.rs:316,319,336
Problem: The byte-offset discriminator path in
materialize_union_packed uses plain + for offset + disc_offset:
let disc_value = match disc_type {
AlkTypeKind::Uint8 => {
data_access::read_u8(buffer, offset + disc_offset, field_path)? as u32
}
AlkTypeKind::Uint16 => {
data_access::read_u16(buffer, offset + disc_offset, field_path, endian)? as u32
}
...
};
let variant_offset = offset + disc_offset + disc_type.type_size().unwrap_or(1);
offset + disc_offset can overflow usize. The sibling implementation
in sequential_reader.rs::read_union_value (lines 416-424) correctly
uses checked_add. The materializer should match.
Low severity because:
offsetcomes from the materializer's own cursor (always small in practice).disc_offsetcomes from the schema (discriminator.offset), which is attacker-controllable in a "load untrusted schema" scenario — but the crate doesn't currently document whether loading untrusted schemas is a supported use case.
Consistency with sequential_reader.rs is the main argument for
fixing this now.
Fix: replace offset + disc_offset with
offset.checked_add(disc_offset).ok_or_else(|| ...)? in both spots.
~6 lines, mirrors the existing sequential_reader.rs pattern.
L2. Three unreachable!() in production code
Files: src/offset_map.rs:277, src/layout_builder.rs:285,
src/materialize.rs:324
Problem: Three match arms use unreachable!() to assert
exhaustiveness over AlkTypeKind:
offset_map.rs:277:_ => unreachable!("all AlkTypeKind variants are covered above")layout_builder.rs:285: samematerialize.rs:324:_ => unreachable!("disc_type restricted by parse_discriminator")
These are all genuinely unreachable given the match arms above them —
the AlkTypeKind enum is fully covered in each case, and
parse_discriminator restricts disc_type to Uint8/Uint16/Uint32
before this point. This is the idiomatic Rust pattern for
exhaustiveness and the compiler will warn if a new variant is added
without updating the match.
Not a bug. But for a library that may eventually load schemas from
untrusted input, an unreachable! is a panic in production. The
defense-in-depth alternative is to return an AlkTypeError::Schema
instead, so a future enum extension (or a logic bug in
parse_discriminator) produces an error rather than a panic.
Fix (optional): replace each unreachable!(msg) with
Err(AlkTypeError::Schema(format!("internal: {msg}"))). The compiler
still warns on non-exhaustive matches (the _ arm catches nothing
once all variants are listed), so this doesn't lose the exhaustiveness
check. ~3 lines per site. Defer if the "untrusted schema" use case
isn't on the v0.1.0 roadmap.
L3. AlkTypeError::source() returns None for Validation variant
File: src/error.rs:45
Problem: AlkTypeError implements the blanket
impl std::error::Error for AlkTypeError {}, so source() always
returns None. The Validation variant wraps a
jsonschema::ValidationError<'static>, which itself implements
std::error::Error and may carry a cause chain.
Downstream consumers (the alkcall logging/diagnostics layer) may want
to walk the cause chain for structured error reporting. Currently
they can only Display the ValidationError (flattened into the
AlkTypeError::Display string), not traverse it.
Verified: jsonschema::ValidationError implements
std::error::Error (in jsonschema-0.46/src/error.rs), so the
source() override is sound.
Fix:
impl std::error::Error for AlkTypeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AlkTypeError::Validation(e) => Some(e),
_ => None,
}
}
}
Note the lifetime: AlkTypeError::Validation holds a
ValidationError<'static>, so the + 'static bound in source() is
satisfied. The existing source_returns_none_for_all_variants test
(in error.rs tests) asserts source().is_none() for Schema/
Offset/Access — those still pass. The Validation arm needs a new
assertion (source().is_some()).
Lift: small ergonomics win for downstream consumers; ~6 lines + ~5 lines of test. Defer if no consumer needs cause-chain walking yet, but cheap to do now.
N1. is_rfc3339_timestamp is a non-strict hand-rolled validator
File: src/validation.rs:351-385
Problem: The is_rfc3339_timestamp function is a hand-rolled
datetime validator. It checks year > 0, month 1..=12, day 1..=31,
hour 0..=23, minute 0..=59 — but:
- Day-of-month per month is not validated (Feb 31, Apr 31 pass).
- Seconds are not checked against 0..=59 (the code checks minutes but
not seconds — line 384 only validates
time_parts[1], nottime_parts[2]if present). - The
rfind('-')timezone-offset heuristic at line 363 usespos >= 8to distinguish a date-separator-from a timezone offset-. This works for2026-07-20T10:30:00-05:00but is fragile for unusual-but-valid inputs.
The function is documented as "Simple RFC 3339 / ISO 8601 datetime
check", so the non-strictness is acknowledged. For 0.1.0 this is
acceptable — AlkType:Timestamp is a length-prefixed string at the
binary level, and strict RFC 3339 validation is the consumer's
responsibility if they need it.
The canonical fix would be to use the chrono or time crate's
parsing, but that adds a dependency. Not worth it for 0.1.0.
Fix (optional): add a doc comment noting the non-strictness explicitly, so consumers on docs.rs know not to rely on it for strict validation. ~2 lines of doc.
N2. SequentialReader returns FieldValue::Bytes for AlkType:Record
File: src/sequential_reader.rs:716
Problem: read_record_value returns
FieldValue::Bytes(&buffer[offset..position]) — a raw byte slice —
for a Record field. The other composite kinds return typed
descriptors (Struct { start, end }, Union { discriminator, variant_start }, Array { count, element_start, element_stride }).
The doc comment on read_record_value says "the consumer recurses
into the record's value schema", so the behavior is documented. But
returning FieldValue::Bytes rather than a FieldValue::Record { start, end, count } is a minor API ergonomics smell: the consumer has to
know that a Record field comes back as Bytes, while every other
composite comes back as a typed variant.
Not a bug. Not worth fixing for 0.1.0 (would require adding a
FieldValue::Record variant and updating consumers). Flagged for
awareness — if the alkcall consumer finds the Record API awkward,
revisit in a follow-up.
Fix: none for 0.1.0. Document as a known API asymmetry.
What's Good
The crate is in notably good shape for a 0.1.0. Highlights:
- Overflow safety is thorough:
checked_addeverywhere in hot paths (data_access,sequential_reader,layout_builder,materialize). This is rare and good — most crates use+and panic on overflow. M2 is the one place this discipline slipped (theas u32cast). - Zero-copy reads:
read_string/read_bytesreturn borrowed slices — no allocation in the read path. Critical for the protocol- parsing use case. - Error attribution: every
Access/Offseterror carries afield_pathstring. Excellent for debugging wire-format issues. - Bounds checks before slicing:
check_boundsthenget(..)withok_or_else— defensive, no panics on bad offsets. build_validatorregisters all 19 keywords — no silent passthrough where anAlkType:*kind is accepted but not validated.- Tests are excellent: 438 tests, good coverage of error paths and edge cases (short buffers, unknown discriminators, invalid UTF-8, both endiannesses, overflow guards where reachable). The test discipline is high.
- wasm32 works — confirmed clean build. The alkcall consumer's napi/wasm adapter path is open.
- No
unsafe, noTODO/FIXME— clean codebase hygiene. - Error type is well-designed: four variants covering the three
engine phases + validation, with
Displaycarrying the field path. L3 is a small ergonomics gap, not a design flaw.
Recommended Order
- M2 (u32 truncation in
write_bytes) — 3 lines, closes a silent-corruption path. Do this first; it's the smallest and highest-value fix. - M1 (
Definitions::merge_into) — ~15 lines + ~15 lines of tests, removes a silent-data-loss path and adds coverage to an untested public API. Do this before publish sincemerge_intois referenced inbuilder.mdExample 3. - L1 (materializer unchecked arithmetic) — ~6 lines, mirrors
the existing
sequential_reader.rspattern. Cheap to do alongside M2. - L3 (
AlkTypeError::source) — ~6 lines + ~5 lines of tests. Small ergonomics win; cheap to do now. - L2 (
unreachable!→Err) — ~9 lines across three sites. Optional; defer if the "untrusted schema" use case isn't on the v0.1.0 roadmap. - N1, N2 — documentation only; defer to the docs sweep.
After M1, M2, L1, and L3, the crate is ready for the pre-publish sanity check (README, inline docs, docs.rs render) and publish.
Notes
- All line numbers refer to the tree at commit
c0217d9(the last commit onmainat review time). - The wasm32-unknown-unknown target was verified as a clean build.
The crate's only dependencies (
jsonschemawithdefault-features = false,serde_jsonwithpreserve_order) are wasm-compatible — nostd::time, no filesystem, no threads. - This review does not cover documentation quality (README, inline docs, docs.rs rendering). Per the publisher's workflow, that's a separate sweep after the code is settled.
- Coverage gaps from review #001 (S4 sequential reader error paths, S8 overflow guards) are not re-litigated here. They remain coverage gaps, not correctness issues.
Resolution (2026-08-11)
Four of the seven findings were resolved in the same session as the
review. 5 new tests added (391 → 396 crate tests; 438 → 443 total
with integration tests). cargo test, cargo clippy --all-targets -- -D warnings, and cargo build --target wasm32-unknown-unknown --release all green.
M2 (u32 truncation in write_bytes) — resolved
src/data_access.rs: added a u32::try_from(data_len) guard before
the u32_to call. A >4GiB blob now returns
AlkTypeError::Access { reason: "data length N exceeds u32::MAX (length prefix width)" } instead of silently writing a truncated
length prefix. ~3 lines.
M1 (Definitions::merge_into silent data loss) — resolved
src/builder.rs: rewrote merge_into to access self.defs directly
instead of round-tripping through self.build(). The double-
cloned().unwrap_or_default().as_object().cloned().unwrap_or_default()
chain is gone — the definitions are moved directly into the target's
$defs object. 4 new tests cover: insert-when-absent, merge-into-
existing, overwrite-duplicate-keys, and no-op-on-non-object-top.
~15 lines + ~50 lines of tests.
L1 (materialize.rs unchecked offset arithmetic) — resolved
src/materialize.rs: the byte-offset discriminator arm of
materialize_union_packed now uses checked_add for both
offset + disc_offset and disc_abs_offset + disc_size, returning
AlkTypeError::Access on overflow. Mirrors the existing pattern in
sequential_reader.rs::read_union_value. The variant_offset
computation is now overflow-safe. ~15 lines.
L3 (AlkTypeError::source() for Validation) — resolved
src/error.rs: replaced the blanket impl std::error::Error for AlkTypeError {} with an explicit impl that returns Some(inner) for
the Validation variant and None for the others. The existing
source_returns_none_for_all_variants test was split into two:
source_returns_none_for_schema_offset_access (unchanged behavior)
and source_returns_some_for_validation_variant (new). ~6 lines +
~10 lines of tests.
Deferred → Resolved (docs sweep)
- N1 (non-strict
is_rfc3339_timestamp): documented as non-strict in the function's doc comment. Lists the specific gaps (day-of-month per month, seconds range, leap seconds) and points consumers needing strict validation tochronoortime. ~10 lines of doc insrc/validation.rs. A strict implementation would add a dependency, not worth it for 0.1.0. - N2 (
FieldValue::BytesforRecord): documented as a known asymmetry in theFieldValueenum doc and onread_record_value. Notes that every other composite kind returns a typed descriptor whileRecordreturnsBytes, and flags the possibility of a futureFieldValue::Recordvariant. ~12 lines of doc acrosssrc/sequential_reader.rs.
L2 (unreachable! → Err) — resolved (follow-up)
The three unreachable! sites were originally deferred. After
discussion with the publisher, the immediate downstream consumer
(alkcall) accepts schemas from arbitrary internet peers in its
hub/spoke topology — there is no way to guarantee the remote side
won't produce a malicious or unsupported schema definition. A panic
is the wrong failure mode for that threat model; an Err the caller
can handle is correct.
All three sites converted to Err(AlkTypeError::Offset/Schema(...)):
offset_map.rs:277—_ => Err(Offset { ... "unsupported AlkType kind for aligned offset computation" })layout_builder.rs:285—_ => Err(Offset { ... "unsupported AlkType kind for packed layout computation" })materialize.rs:324—_ => return Err(Schema(... "internal: union discriminator type N is not a supported byte discriminator (parse_discriminator should have rejected this)"))
The materialize.rs site uses Schema rather than Access because
a wrong disc_type is a schema-authoring bug (the parse_discriminator
validator should have caught it earlier), not a buffer-access error.
The offset_map and layout_builder sites use Offset to match
their surrounding error variants. The sibling sites in
sequential_reader.rs::read_byte_discriminator (line 535) and
tunion.rs::read_byte_discriminator (line 70) already returned
Err(Schema(...)) — only materialize.rs was the holdout.
Note: the k if k.is_fixed_size() guard pattern in offset_map and
layout_builder means the compiler cannot enforce exhaustiveness on
these match arms (the guard catches new variants at runtime, not
compile time). Converting the _ arm from unreachable! to Err
is the defense-in-depth mitigation: a new AlkTypeKind variant added
without updating these matches produces a handled error, not a panic.
A future refactor could remove the is_fixed_size() guard and list
all fixed-size kinds explicitly — that would restore compile-time
exhaustiveness checking. Deferred to a separate cleanup pass.
Verified: no unreachable! remains in production code (the one
remaining hit at offset_map.rs:688 is inside a #[test] fn,
guarded by assert!(matches!(...)) on the line above).
After M1, M2, L1, L2, and L3, the remaining open findings (N1, N2) were resolved in the pre-publish docs sweep. All 7 findings are now closed. The crate is ready for the final sanity check and publish.