Files
deepseek-v4-pro 510553d800 Remove the Timestamp kind
The Timestamp kind was a residual from an early research reference. It
was byte-identical to String everywhere (length-prefixed UTF-8) and its
only distinguishing behavior was a hand-rolled non-strict RFC 3339 check
that the docs admitted was incomplete (Feb 31 passes, seconds range
unchecked, no leap seconds). JSON-level timestamp validation is
jsonschema's job (format: date-time on the validate_json path), not
alktype's.

Removes the AlkTypeKind::Timestamp variant, its to_bast_str/from_bast_str
mapping, the builder's Schema::timestamp() constructor, the
validate_timestamp/is_rfc3339_timestamp validator arms, and the
materializer/reader/engine timestamp arms. Updates the meta-schema
primitive enum (14 -> 13), the spec docs (bast-format.md, schema-layer.md,
builder.md, validation.md, overview.md, data-access.md, README.md), and
the kind-count references (19 -> 18).

Verification: cargo test --release (407 pass), cargo clippy --all-targets
-- -D warnings (clean), cargo doc --no-deps (clean), cargo build --target
wasm32-unknown-unknown --release (clean).
2026-08-16 09:12:33 +00:00

27 KiB

status, last_updated
status last_updated
accepted 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; resolves OQ-003. The two-output-format split is D-BAST-008, recorded in ADR-BAST.

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 (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 for the kinds and 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 for the decision rationale (why Value not a typed Schema enum, why both BAST and standard JSON Schema in one builder) and ADR-BAST 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.

// 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). 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 §"The 18 BAST Kinds"):

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 §"The 18 BAST Kinds"):

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):

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:

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). Each setter is named after the annotation it produces; calling the setter sets the corresponding JSON key. Setters return Self for chaining.

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:

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:

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": <field.build()>, ... } 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 §"What the builder is not"). Calling required multiple times accumulates names:

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.):

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).

.build()

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

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:

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):

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):

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).

pub struct Definitions { /* ... */ }

impl Definitions {
    pub fn new() -> Self;

    /// Define a named schema. Returns a `Schema` that produces
    /// `{"$ref": "#/$defs/<name>"}` — 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):

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:

let op = Schema::object()
    .field("error", defs.define("FileNotFound", /* ... */))
    .build();

Schema::ref_def — reference a definition by name

impl Schema {
    /// Produce a `{"$ref": "#/$defs/<name>"}` 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/<name>"} form.

Usage Examples

Example 1: channels' 8-byte chunk header (binary layout, BAST)

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)

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 $refs resolve:

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)

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 Fluent Rust API producing serde_json::Value; covers BAST kinds + standard JSON Schema; resolves OQ-003
BAST format + two output formats ADR-BAST struct_() → BAST, object() → standard JSON Schema (D-BAST-008)
Schema annotations ADR-003 The annotation semantics the builder's setters produce (location moved to BAST type-level properties)
Load-time validation strategy ADR-004 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.

References

  • ADR-009 — the decision this spec implements
  • ADR-BAST — the BAST format and the two-output-format decision (D-BAST-008)
  • ADR-001 — scope boundaries this module extends; "schemas are JSON" principle
  • ADR-003 — the annotation semantics the builder's setters produce
  • bast-format.md — the normative BAST format specification (the output format for struct_())
  • schema-layer.md — the BAST kinds and parser
  • 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)