--- id: typedef/layout-builder name: Implement packed sequential LayoutBuilder for protocol write-side status: pending depends_on: [typedef/schema-types, typedef/error-type, typedef/data-access] scope: moderate risk: medium impact: component level: implementation --- ## Description Implement the packed sequential `LayoutBuilder` in `crates/alknet-typedef/src/layout_builder.rs`. This is the write-side of Mode 1 (ADR-096): fields are packed with no alignment padding. Variable-length fields shift all subsequent fields. The consumer provides actual data sizes for variable-length fields; the builder computes byte positions for each field. Per [layout-engine.md](../../docs/architecture/crates/typedef/layout-engine.md) §"Mode 1: Packed sequential". ### Target shape ```rust /// Builds a packed sequential layout for protocol wire formats. /// Fields are packed with no alignment padding. Variable-length fields /// shift all subsequent fields. The consumer provides actual data sizes /// for variable-length fields to compute correct positions. /// /// Used at write time when the consumer knows the data sizes upfront. #[derive(Debug)] pub struct LayoutBuilder { /// The schema being laid out. schema: serde_json::Value, /// The endianness for the layout. endian: Endian, } /// A field position computed by the LayoutBuilder. #[derive(Debug, Clone)] pub struct FieldPosition { /// Byte offset of the field within the buffer. pub offset: usize, /// Byte size of the field (4 for length prefix of variable-length fields, /// actual size for fixed-size fields). pub size: usize, /// The TypeDef kind of the field. pub kind: String, } /// The result of building a layout: a map of field_path → FieldPosition /// and the total buffer size needed. #[derive(Debug)] pub struct PackedLayout { fields: Vec<(String, FieldPosition)>, total_size: usize, } impl LayoutBuilder { /// Create a new LayoutBuilder from a schema. pub fn new(schema: &serde_json::Value) -> Result; /// Build the packed layout given actual data sizes for variable-length fields. /// `var_sizes` maps field paths to their actual byte sizes (not including /// the 4-byte length prefix — the builder adds that). /// /// For fixed-size fields, the size is known from the schema. /// For variable-length fields, the size comes from `var_sizes`. /// For TUnion, the consumer provides the discriminator value to select /// the variant, and the variant's field sizes. pub fn build( &self, var_sizes: &HashMap, ) -> Result; } impl PackedLayout { /// Look up a field's position by dotted path. pub fn get(&self, field_path: &str) -> Option<&FieldPosition>; /// The total buffer size needed to hold all fields. pub fn total_size(&self) -> usize; /// Iterate over all (field_path, position) pairs in layout order. pub fn iter(&self) -> impl Iterator; } ``` ### How it works For a struct with fields `[u8, u32, string]` where the string is 10 bytes: ``` LayoutBuilder::build(var_sizes: {"payload": 10}): field[0] u8: offset 0, size 1 field[1] u32: offset 1, size 4 field[2] string: offset 5, size 4 (length prefix) + 10 (data) = 14 total: 19 ``` There is no alignment padding. The `u32` at offset 1 is unaligned — this is correct for protocol wire formats, which pack fields tightly. ### Variable-length fields in packed mode The `LayoutBuilder` takes actual data sizes for variable-length fields to compute correct positions for subsequent fields. The consumer must know the data sizes before writing — this is inherent to packed layouts. For each variable-length field: 1. The builder records the position of the 4-byte length prefix. 2. The builder adds `4 + data_size` to the current offset. 3. Subsequent fields start after the variable data. ### TUnion in packed mode For `TUnion`, the consumer provides the discriminator value and the variant's field sizes. The builder: 1. Computes the discriminator's position and size. 2. Looks up the variant schema from the mapping. 3. Computes the variant's field positions starting at `offset + discriminator_size`. 4. The union's total size is `discriminator_size + variant_size`. ### What this does NOT include - The read-side of packed mode (that's `sequential_reader.rs`) - Aligned static layout (that's `offset_map.rs`) - TUnion discriminator dispatch (that's `tunion.rs`) - The `TypedefEngine` struct (that's `engine.rs`) ## Acceptance Criteria - [ ] `LayoutBuilder` struct with `new(schema)` constructor - [ ] `LayoutBuilder::build(var_sizes)` computes packed field positions - [ ] Fixed-size fields get correct offsets with no alignment padding - [ ] `u8` at offset 0, `u32` at offset 1 (no padding) — packed sequential - [ ] Variable-length fields: 4-byte length prefix at computed offset, data follows - [ ] Variable-length field sizes come from `var_sizes` map - [ ] Subsequent fields shift based on actual variable-length data sizes - [ ] Nested structs produce dotted field paths - [ ] `TArray` of fixed-size elements: correct stride (element size, no padding) - [ ] `TArray` with variable count: 4-byte count prefix at computed offset - [ ] `TUnion` with byte-offset discriminator: discriminator at `offset`, variant at `offset + disc_size` - [ ] `TUnion` with field-name discriminator: discriminator is a regular field - [ ] `PackedLayout::get("field_name")` returns correct `FieldPosition` - [ ] `PackedLayout::total_size()` returns correct total buffer size - [ ] `PackedLayout::iter()` iterates fields in layout order - [ ] Returns `TypedefError::Schema` for malformed schemas - [ ] Returns `TypedefError::Offset` for missing variable-length field sizes - [ ] No `unwrap()` or `expect()` on error paths - [ ] All public types and functions have doc comments - [ ] `cargo check -p alknet-typedef` succeeds - [ ] `cargo clippy -p alknet-typedef` succeeds with no warnings - [ ] `cargo build --workspace` still succeeds ## References - docs/architecture/crates/typedef/layout-engine.md — Mode 1: Packed sequential, LayoutBuilder - docs/architecture/crates/typedef/schema-layer.md — the 17 TypeDef kinds and their byte sizes - docs/architecture/decisions/096-two-layout-modes-packed-vs-aligned.md — ADR-096 - docs/architecture/decisions/097-schema-annotations.md — ADR-097 (encoding) - /workspace/alknet-typedef-poc/src/offset.rs — POC reference (LayoutBuilder in POC 2) ## Notes > This is the write-side of the packed sequential mode. The consumer knows the data > sizes upfront (e.g., when constructing an SFTP response packet) and uses the > `LayoutBuilder` to compute where each field goes. The builder does not write data — > it only computes positions. The consumer uses the `data_access` module's write > functions at the computed positions. The POC 2's `LayoutBuilder` is a good reference. ## Summary > To be filled on completion