--- status: accepted last_updated: 2026-08-15 --- # alktype — Builder API The builder layer: a fluent Rust API for constructing BAST documents (binary-layout schemas) and standard JSON Schemas (JSON-validation schemas) at runtime, producing `serde_json::Value`. Decided in [ADR-009](decisions/009-builder-api.md); resolves [OQ-003](questions/003-builder-api-for-schema-construction.md). The two-output-format split is D-BAST-008, recorded in [ADR-BAST](decisions/bast-bast-format.md). ## What The `builder` module provides a single `Schema` builder type and a `Definitions` helper for named `$defs`. The builder's `.build()` method returns a `serde_json::Value` — one of two forms depending on the constructor used (D-BAST-008): - **BAST JSON** (binary layout) — `Schema::struct_().field(...).build()` produces a BAST TypeDef (`{ "kind": "struct", "fields": [...] }`). Primitive constructors produce bare TypeRef strings (`"uint32"`). Feed to [`AlkTypeEngine::compile`](validation.md) (the binary-layout path) → `validate_bytes`. - **Standard JSON Schema** (JSON validation) — `Schema::object().field(...)` produces `{ "type": "object", "properties": {...}, "required": [...] }`. No BAST `kind`, no custom keywords — a plain JSON Schema. Feed to a standard `jsonschema::Validator` (or `AlkTypeEngine::compile` with a JSON Schema for the `validate_json` path, D-BAST-007). The builder covers: - All 18 BAST kinds (binary-layout schemas) — see [schema-layer.md](schema-layer.md) for the kinds and [`bast-format.md`](bast-format.md) for the format. - All standard JSON Schema keywords needed for operation payload schemas: `type`, `properties`, `required`, `items`, `enum`, `format`, `additionalProperties`, `minimum`, `maximum`, `minItems`, `maxItems`, `minLength`, `maxLength`, `$ref`, `$defs`. - All ADR-003 schema annotations: `endian`, `align`, `encoding` (length-prefixed / offset-indirect), `maxLength`-as-reservation, and TUnion `discriminator` (byte-offset and field-name). ## Why alkcall (the merged `alknet-call` + `alknet-channels` extraction) is alktype's first consumer and needs to build schemas at runtime from Rust code, for two roles: 1. **Binary layout schemas** (channels' 8-byte chunk header, future binary call frames) — BAST documents, fed to `AlkTypeEngine::compile` (packed mode, big-endian) → `validate_bytes`. 2. **JSON payload schemas** (call's `OperationSpec.input_schema` / `output_schema` / `error_schemas`) — plain JSON Schema, no BAST `kind`, validated via the standard `jsonschema` validator (`AlkTypeEngine::compile` with a JSON Schema → `validate_json`). A single builder serving both roles means alkcall imports one module for schema construction. See [ADR-009](decisions/009-builder-api.md) for the decision rationale (why `Value` not a typed `Schema` enum, why both BAST and standard JSON Schema in one builder) and [ADR-BAST](decisions/bast-bast-format.md) for the two-output-format decision (D-BAST-008). ## Architecture ### Output type: `serde_json::Value` The builder returns `serde_json::Value`. There is no typed `Schema` enum. This matches ADR-001's "schemas are JSON" principle and avoids duplicating the JSON form that `AlkTypeEngine::compile`, `jsonschema::Validator`, and `OperationSpec` all consume. ### Module placement `src/builder.rs`, re-exported from the crate root. The builder is a peer of `bast.rs` (which parses BAST documents) and `engine.rs` (which compiles them). The builder constructs; it does not parse or compile. ```rust // src/lib.rs (additions) pub mod builder; pub use builder::{Schema, Definitions, Discriminator}; ``` ### Field order is explicit BAST struct fields are an ordered array (BAST design principle #4 — see [`bast-format.md`](bast-format.md#design-principles)). The builder's `struct_()`/`union_()` accumulates fields in call order and emits them as the `fields` array on `.build()`. Field order in the builder is the field order in the binary layout. This is critical for packed mode (ADR-002) where field order determines offsets. (`serde_json`'s `preserve_order` feature remains a dependency, but layout correctness no longer depends on it — the `fields` array makes order explicit. `preserve_order` is still load-bearing for the `mapping` object's iteration order and for `Definitions`' `$defs` block, which the parser walks in document order.) ## Public API ### `Schema` builder `Schema` is the single entry point. Constructors for each BAST kind and each standard JSON Schema type; setters for annotations and constraints; `.build()` produces `Value`. #### BAST kind constructors One constructor per `AlkTypeKind` variant (see [schema-layer.md](schema-layer.md) §"The 18 BAST Kinds"): ```rust impl Schema { // Fixed-size integer kinds pub fn int8() -> Self; pub fn int16() -> Self; pub fn int32() -> Self; pub fn int64() -> Self; pub fn uint8() -> Self; pub fn uint16() -> Self; pub fn uint32() -> Self; pub fn uint64() -> Self; // Float kinds pub fn float32() -> Self; pub fn float64() -> Self; // Other fixed-size pub fn boolean() -> Self; pub fn enum_of(values: &[&str]) -> Self; // u32 index; values in declaration order // Variable-length kinds pub fn string() -> Self; pub fn bytes() -> Self; // Composite kinds pub fn struct_() -> Self; // fields added via .field() pub fn union_(disc: Discriminator) -> Self; // variants via .mapping() pub fn array_of(element: Schema) -> Self; // .count() required for valid BAST (D-BAST-004) pub fn record_of(value: Schema) -> Self; } ``` Primitive constructors produce the bare BAST TypeRef string on `.build()`. For example, `Schema::uint32().build()` produces `"uint32"`. Composite constructors produce the BAST object form. **`enum_of`** produces a BAST enum TypeDef (`{ "kind": "enum", "values": [...] }`); declaration order is the index order — see [schema-layer.md](schema-layer.md) §"The 18 BAST Kinds"): ```rust Schema::enum_of(&["read", "write", "execute"]).build() // -> { "kind": "enum", "values": ["read", "write", "execute"] } ``` **`array_of`** and **`record_of`** take the element/value schema as a nested `Schema`. `array_of` requires `.count(N)` for valid BAST (D-BAST-004 — arrays of variable-length elements without a count are deferred, aligning with OQ-001): ```rust Schema::array_of(Schema::uint32()).count(3).build() // -> { "kind": "array", "element": "uint32", "count": 3 } Schema::record_of(Schema::float32()).build() // -> { "kind": "record", "values": "float32" } ``` #### Standard JSON Schema type constructors For plain JSON Schema (no BAST `kind`) — call's `input_schema` / `output_schema` / `error_schemas`: ```rust impl Schema { pub fn object() -> Self; // type: "object" — fields via .field() pub fn array() -> Self; // type: "array" — items via .items() pub fn string_() -> Self; // type: "string" pub fn integer() -> Self; // type: "integer" pub fn number() -> Self; // type: "number" pub fn boolean_()-> Self; // type: "boolean" pub fn null() -> Self; // type: "null" pub fn any() -> Self; // {} (no type constraint) } ``` The `_` suffix disambiguates standard JSON Schema types from BAST kinds (`string` is the BAST primitive; `string_` is the standard JSON Schema type — `string()` would produce `"string"` as a BAST TypeRef, `string_()` produces `{ "type": "string" }` as a standard JSON Schema). This is deliberate: the two are distinct schema forms and the builder makes the distinction visible at the call site. #### Annotation setters Annotation setters mirror ADR-003 (semantics unchanged; location moved to BAST type-level properties under the pivot — see [ADR-BAST](decisions/bast-bast-format.md)). Each setter is named after the annotation it produces; calling the setter sets the corresponding JSON key. Setters return `Self` for chaining. ```rust impl Schema { /// Struct/union-level endianness (ADR-003 §1). Default little. pub fn endian(mut self, endian: Endian) -> Self; /// Struct or field alignment (ADR-003 §2). Struct-level sets the /// default; field-level overrides. pub fn align(mut self, align: usize) -> Self; /// Variable-length encoding strategy (ADR-003 §3). /// Default length-prefixed. pub fn encoding(mut self, encoding: VariableEncoding) -> Self; /// maxLength — standard JSON Schema keyword. In aligned mode with /// a variable-length type, reserves this many bytes (strategy 2). /// In packed mode, validation constraint only. pub fn max_length(mut self, max: usize) -> Self; /// Array count (D-BAST-004 — required for valid BAST arrays in v1). pub fn count(mut self, count: usize) -> Self; } ``` `Endian` and `VariableEncoding` are re-exported from `schema.rs` (no new types — the builder uses the existing enums). When applied to a struct, `endian`/`align` are struct-level; when the `Schema` is used as a `.field()` argument, the builder extracts `endian`/`align`/`encoding`/ `maxLength` and places them on the *field* object (BAST field-level properties). The setters produce the exact BAST JSON shapes from [`bast-format.md`](bast-format.md): ```rust Schema::struct_() .endian(Endian::Big) .field("channel_id", Schema::uint32()) .field("length", Schema::uint32()) .build() // -> { // "kind": "struct", // "endian": "big", // "fields": [ // { "name": "channel_id", "kind": "uint32" }, // { "name": "length", "kind": "uint32" } // ] // } ``` #### Composite builders `struct_()`, `union_()`, `array_of()`, `record_of()` are the composite constructors. `struct_()` and `union_()` need additional setters to populate their children: ```rust impl Schema { /// Add a field to a struct (or a field-name-discriminator union). /// Field order is load-bearing for binary layouts (packed mode /// field order = byte order — the `fields` array is ordered). /// The field's schema is built from the passed `Schema`. pub fn field(mut self, name: &str, field: Schema) -> Self; /// Mark fields as required (standard JSON Schema `required` keyword). /// Only meaningful for `object()` (standard JSON Schema) — BAST /// structs require all declared fields present (the validator /// enforces this). Can be called multiple times; required names /// accumulate. pub fn required(mut self, names: &[&str]) -> Self; /// Set the items schema for a standard `array` type. pub fn items(mut self, item: Schema) -> Self; /// Set the additionalProperties schema for a standard `object` type. pub fn additional_properties(mut self, props: Schema) -> Self; /// Add a variant to a union. `disc_value` is the stringified /// discriminator value (mapping key). The variant schema is built /// from the passed `Schema`. pub fn mapping(mut self, disc_value: &str, variant: Schema) -> Self; } ``` **`field`** appends a `{ "name": ..., "kind": , ... }` entry to the struct/union's `fields` array, extracting field-level annotations (`endian`, `align`, `encoding`, `maxLength`) from the passed `Schema`. Repeated calls append in order. Field order in the built `Value` is the call order. **`required`** sets the standard JSON Schema `"required"` array. The builder does not check that the named fields exist (that's a compile-time check at `AlkTypeEngine::compile`, per ADR-004's load-time validation strategy — see [ADR-009](decisions/009-builder-api.md) §"What the builder is not"). Calling `required` multiple times accumulates names: ```rust Schema::object() .field("path", Schema::string_().max_length(4096)) .field("offset", Schema::integer().minimum(0)) .field("length", Schema::integer().minimum(0)) .required(["path"]) .required(["offset", "length"]) .build() // -> { // "type": "object", // "properties": { "path": {...}, "offset": {...}, "length": {...} }, // "required": ["path", "offset", "length"] // } ``` #### Constraint setters (standard JSON Schema) For operation payload schemas (call's `input_schema` etc.): ```rust impl Schema { /// `minimum` (inclusive lower bound for numbers/integers). pub fn minimum(mut self, min: f64) -> Self; /// `maximum` (inclusive upper bound for numbers/integers). pub fn maximum(mut self, max: f64) -> Self; /// `minLength` (minimum string length). pub fn min_length(mut self, min: usize) -> Self; /// `minItems` (minimum array length). pub fn min_items(mut self, min: usize) -> Self; /// `maxItems` (maximum array length). pub fn max_items(mut self, max: usize) -> Self; /// `format` (e.g. "date-time", "uri", "email"). pub fn format(mut self, fmt: &str) -> Self; /// `title` (human-readable description). pub fn title(mut self, t: &str) -> Self; /// `description` (human-readable description). pub fn description(mut self, d: &str) -> Self; } ``` These set the corresponding standard JSON Schema keywords. They apply to standard JSON Schema type schemas (e.g., `Schema::string_().max_length(4096)` sets `maxLength`, which on the `validate_json` path is a JSON-Schema validation constraint). On a BAST schema, `max_length` also serves as the aligned-mode fixed-size reservation (ADR-003 §3) and the packed-mode validation constraint (enforced by the BAST-native validator — see [validation.md](validation.md)). #### `.build()` ```rust impl Schema { /// Produce the final `serde_json::Value`. pub fn build(self) -> Value; } ``` Consumes the builder and returns the assembled `Value`. The builder is not `Clone` (to discourage partial builds); each `Schema` is consumed once. To reuse a sub-schema, build it once and pass the `Value` to a `Schema::from_value` constructor (below). #### `Schema::from_value` — adopt an existing Value ```rust impl Schema { /// Adopt an existing JSON Schema `Value` as a `Schema`, for /// composition with builder-constructed schemas. Does not validate /// the schema; just wraps it. pub fn from_value(value: Value) -> Self; } ``` For consumers that have some schemas as JSON (e.g., loaded from a TypeBox-produced file) and others built via the fluent API, and want to compose them. `from_value` wraps the `Value` so it can be passed to `.field()` / `.items()` / `.mapping()` like any other `Schema`. ### `Discriminator` for `union_()` `union_()` takes a `Discriminator` describing the union's dispatch mechanism. This mirrors `bast::BastDiscriminator` (the parser's typed view) but with a builder-friendly shape: ```rust pub enum Discriminator { /// Byte-offset discriminator (ADR-003 §4 Kind A). /// `offset` is the byte position; `disc_type` is the BAST kind /// of the discriminator (Uint8/Uint16/Uint32). Byte { offset: usize, disc_type: AlkTypeKind, // restricted to Uint8/Uint16/Uint32 }, /// Field-name discriminator (ADR-003 §4 Kind B). /// `name` is the field holding the discriminator value. The /// discriminator field and any shared fields are declared via /// `.field()` on the union builder. Field { name: String, }, } ``` **Byte-offset example** (channels-style protocol dispatch): ```rust let packet = Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("5", Schema::ref_def("Read")) .mapping("6", Schema::ref_def("Write")) .mapping("101", Schema::ref_def("Status")) .build(); // -> { // "kind": "union", // "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, // "mapping": { // "5": { "$ref": "#/$defs/Read" }, // "6": { "$ref": "#/$defs/Write" }, // "101": { "$ref": "#/$defs/Status" } // } // } ``` **Field-name example** (typedef.ts pattern): ```rust let event = Schema::union_(Discriminator::Field { name: "type" }) .field("type", Schema::string()) .mapping("read", Schema::ref_def("Read")) .mapping("write", Schema::ref_def("Write")) .build(); // -> { // "kind": "union", // "discriminator": { "kind": "field", "name": "type" }, // "fields": [ { "name": "type", "kind": "string" } ], // "mapping": { "read": {...}, "write": {...} } // } ``` (Field-name-discriminator unions require a `fields` array declaring the discriminator field — D-BAST-005. The builder emits `fields` only when the discriminator is `Field` and at least one field was added.) ### `Definitions` — named `$defs` for cross-reference `Definitions` is a helper for building named `$defs` that schemas can `$ref` by name, and for assembling a complete BAST document. This is the ergonomics win for alkcall's `OperationSpec`, where input/output/error schemas reference shared definitions (e.g., `FileNotFound`, `RateLimited`). ```rust pub struct Definitions { /* ... */ } impl Definitions { pub fn new() -> Self; /// Define a named schema. Returns a `Schema` that produces /// `{"$ref": "#/$defs/"}` — the JSON Pointer form BAST /// requires (no `normalize_refs` step; refs are always full /// pointers). pub fn define(&mut self, name: &str, schema: Schema) -> Schema; /// Like `define`, but the schema is an existing `Value` (adopted /// via `Schema::from_value`). pub fn define_value(&mut self, name: &str, value: Value) -> Schema; /// Produce the `{"$defs": { ... }}` object. pub fn build(self) -> Value; /// Build a complete BAST document with `root_name` as the root /// type. The root schema is inserted into `$defs` alongside any /// previously defined entries. The resulting `Value` is ready for /// `AlkTypeEngine::compile(&doc, root_name, mode, ...)`. pub fn build_doc(self, root_name: &str, root: Schema) -> Value; /// Merge the `$defs` into a top-level schema `Value`. If `top` /// already has a `$defs` object, the definitions are merged into /// it; otherwise a `$defs` key is inserted. For BAST documents, /// prefer `build_doc` — it places the root type inside `$defs` /// (where BAST requires it). pub fn merge_into(self, top: &mut Value); } ``` **Usage (complete BAST document):** ```rust let mut defs = Definitions::new(); defs.define("Init", Schema::struct_().field("version", Schema::uint32())); defs.define("Read", Schema::struct_() .field("handle", Schema::bytes()) .field("offset", Schema::uint64()) .field("len", Schema::uint32())); let doc = defs.build_doc("Packet", Schema::struct_() .field("payload", Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("1", Schema::ref_def("Init")) .mapping("5", Schema::ref_def("Read")))); // -> { // "$defs": { // "Init": { "kind": "struct", "fields": [ { "name": "version", "kind": "uint32" } ] }, // "Read": { "kind": "struct", "fields": [ ... ] }, // "Packet": { "kind": "struct", "fields": [ // { "name": "payload", "kind": { // "kind": "union", // "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, // "mapping": { "1": { "$ref": "#/$defs/Init" }, "5": { "$ref": "#/$defs/Read" } } // } } // ] } // } // } // // Feed to AlkTypeEngine::compile(&doc, "Packet", LayoutMode::Packed, None) // then validate incoming frames via engine.validate_bytes(&frame). ``` `define` returns a `Schema` (the `$ref` to the definition), so it can be passed directly to `.field()` / `.mapping()` / `.items()` without a separate `ref_def` call: ```rust let op = Schema::object() .field("error", defs.define("FileNotFound", /* ... */)) .build(); ``` #### `Schema::ref_def` — reference a definition by name ```rust impl Schema { /// Produce a `{"$ref": "#/$defs/"}` schema. The definition /// must exist in the `$defs` of the top-level schema at compile /// time. The builder does not check this. pub fn ref_def(name: &str) -> Self; } ``` For cases where the `Definitions::define` return value isn't handy (e.g., referencing a definition defined elsewhere). Produces the same `{"$ref": "#/$defs/"}` form. ## Usage Examples ### Example 1: channels' 8-byte chunk header (binary layout, BAST) ```rust use alktype::{Schema, Endian, Definitions}; let chunk_header = Schema::struct_() .endian(Endian::Big) .field("channel_id", Schema::uint32()) .field("length", Schema::uint32()); // Build a complete BAST document (single-type — one $defs entry). let doc = Definitions::new().build_doc("ChunkHeader", chunk_header); // -> { // "$defs": { // "ChunkHeader": { // "kind": "struct", // "endian": "big", // "fields": [ // { "name": "channel_id", "kind": "uint32" }, // { "name": "length", "kind": "uint32" } // ] // } // } // } // // Feed to AlkTypeEngine::compile(&doc, "ChunkHeader", LayoutMode::Packed, None) // then validate incoming frames via engine.validate_bytes(&frame). ``` ### Example 2: call's `OperationSpec` input schema (JSON payload, standard JSON Schema) ```rust use alktype::Schema; let read_file_input = Schema::object() .field("path", Schema::string_().max_length(4096)) .field("offset", Schema::integer().minimum(0)) .field("length", Schema::integer().minimum(0)) .required(["path"]) .build(); // -> { // "type": "object", // "properties": { // "path": { "type": "string", "maxLength": 4096 }, // "offset": { "type": "integer", "minimum": 0 }, // "length": { "type": "integer", "minimum": 0 } // }, // "required": ["path"] // } // // Stored in OperationSpec.input_schema; validated via the standard // jsonschema validator (AlkTypeEngine::compile with Some(&read_file_input) // for the validate_json path, or serde_json::from_slice then // validate_json for wire frames). ``` ### Example 3: SFTP `Packet` union (binary layout, byte discriminator) The SFTP wire shape is `[type:u8][payload-struct]` — a struct with a union payload field. The engine requires a struct at the root (`OffsetMap::compute` / `SequentialReader::new` both enforce this; a `Union` is a field type within a struct, not a top-level schema). The builder constructs the union wrapped in a struct, and `$defs` are placed inside the document via `build_doc` so `$ref`s resolve: ```rust use alktype::{Definitions, Discriminator, AlkTypeKind, Schema}; let mut defs = Definitions::new(); defs.define("Init", Schema::struct_().field("version", Schema::uint32())); defs.define("Open", Schema::struct_().field("path", Schema::string()).field("flags", Schema::uint32())); defs.define("Read", Schema::struct_().field("handle", Schema::bytes()).field("offset", Schema::uint64()).field("len", Schema::uint32())); defs.define("Write", Schema::struct_().field("handle", Schema::bytes()).field("offset", Schema::uint64()).field("data", Schema::bytes())); defs.define("Status", Schema::struct_().field("code", Schema::uint32()).field("message", Schema::string())); // A "Packet" is a struct with one field — the union. This mirrors // SFTP's wire shape: [type:u8][payload-struct]. let doc = defs.build_doc("Packet", Schema::struct_() .field("payload", Schema::union_(Discriminator::Byte { offset: 0, disc_type: AlkTypeKind::Uint8, }) .mapping("1", Schema::ref_def("Init")) .mapping("3", Schema::ref_def("Open")) .mapping("5", Schema::ref_def("Read")) .mapping("6", Schema::ref_def("Write")) .mapping("101", Schema::ref_def("Status")))); // Feed to AlkTypeEngine::compile(&doc, "Packet", LayoutMode::Packed, None) // then validate incoming frames via engine.validate_bytes(&frame). ``` ### Example 4: OperationSpec error schemas (named `$defs`, standard JSON Schema) ```rust use alktype::{Definitions, Schema}; let mut defs = Definitions::new(); let file_not_found = defs.define("FileNotFound", Schema::object() .field("path", Schema::string_()) .field("errno", Schema::integer()) .required(["path", "errno"]) ); let rate_limited = defs.define("RateLimited", Schema::object() .field("retry_after_ms", Schema::integer().minimum(0)) .required(["retry_after_ms"]) ); // An operation's error schemas reference these definitions let op_errors = vec![ ErrorDefinition { code: "FILE_NOT_FOUND".to_string(), description: "File not found".to_string(), schema: Schema::ref_def("FileNotFound").build(), http_status: Some(404), }, ErrorDefinition { code: "RATE_LIMITED".to_string(), description: "Rate limited".to_string(), schema: Schema::ref_def("RateLimited").build(), http_status: Some(429), }, ]; // `$defs` is built once and stored alongside the OperationSpec. // (For the validate_json path, compile with Some(&defs.build()) as the // json_schema argument — but typically OperationSpec schemas are // validated directly via jsonschema, not via AlkTypeEngine.) ``` ## Design Decisions | Decision | ADR | Summary | |----------|-----|---------| | Builder API for schema construction | [ADR-009](decisions/009-builder-api.md) | Fluent Rust API producing `serde_json::Value`; covers BAST kinds + standard JSON Schema; resolves OQ-003 | | BAST format + two output formats | [ADR-BAST](decisions/bast-bast-format.md) | `struct_()` → BAST, `object()` → standard JSON Schema (D-BAST-008) | | Schema annotations | [ADR-003](decisions/003-schema-annotations.md) | The annotation semantics the builder's setters produce (location moved to BAST type-level properties) | | Load-time validation strategy | [ADR-004](decisions/004-error-handling-validation-strategy.md) | The builder does not pre-validate; compile-time is the validation point | ## Open Questions None specific to the builder. OQ-003 (the original "should we build a builder API") is resolved by this spec / ADR-009. OQ-004 (`Discriminator::Field` name type — `&str` or `String`) is resolved: `String`, for ownership simplicity (the builder consumes `Self` on setters; `&str` would require a lifetime parameter on `Discriminator` and transitively on `Schema::union_`). See [open-questions.md](open-questions.md). ## References - [ADR-009](decisions/009-builder-api.md) — the decision this spec implements - [ADR-BAST](decisions/bast-bast-format.md) — the BAST format and the two-output-format decision (D-BAST-008) - [ADR-001](decisions/001-alktype-purpose-scope-jsonschema-engine.md) — scope boundaries this module extends; "schemas are JSON" principle - [ADR-003](decisions/003-schema-annotations.md) — the annotation semantics the builder's setters produce - [`bast-format.md`](bast-format.md) — the normative BAST format specification (the output format for `struct_()`) - [schema-layer.md](schema-layer.md) — the BAST kinds and parser - [validation.md](validation.md) — the validation layer that consumes builder output (via `AlkTypeEngine::compile`) - `@alkdev/alknet: docs/architecture/crates/call/operation-registry.md` — `OperationSpec` (the alkcall consumer for the JSON-payload role) - `@alkdev/alknet: docs/architecture/crates/channels/channels-wire.md` — the 8-byte chunk header (the alkcall consumer for the binary-layout role)