diff --git a/.opencode/agents/implementation-specialist.md b/.opencode/agents/implementation-specialist.md index 62b5216..1a3bb64 100644 --- a/.opencode/agents/implementation-specialist.md +++ b/.opencode/agents/implementation-specialist.md @@ -241,9 +241,16 @@ Read `AGENTS.md` at project root for full details. Key rules: `AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`, `StreamError` live in this crate. Do not add a separate `alkcore` dependency. Keep them lean (no TLS, no transport coupling, no endpoint/accept-loop). -10. **`alktype` dependency** — use `alktype` for binary layout (channels chunk - header, future binary payload schemas) and JSON payload schema validation - (`OperationSpec`'s `input_schema`/`output_schema`). Do not roll your own. +10. **BAST documents for wire formats** — every binary wire format carries a + BAST (Binary Abstract Syntax Tree) document as its machine-readable spec + (e.g. the channels chunk header's `docs/architecture/chunk-header.bast.json`, + embedded as `CHUNK_HEADER_BAST`). BAST is plain JSON — no dependency + required to author or consume it. The `alktype` crate compiles BAST into + readers/writers/validators; future codegen derives language-specific + implementations. Trivial or hot-path formats (chunk header, tty framing) + stay hand-rolled with the BAST doc as the contract; complex formats (sftp) + use the alktype engine or codegen. Do not roll your own offset map or + validator for complex formats. 11. **Feature flags** — transports may be feature-gated if the need arises. The base crate should compile lean (no `quinn`, no `iroh` unless the feature is on). Verify both `cargo test` (default) and `cargo test --all-features` pass diff --git a/AGENTS.md b/AGENTS.md index b33fb41..c9b0b3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,11 +136,18 @@ session, not just spawned implementation agents. dial and the TLS config are concerns of the consumer, not of this crate. See ADR-007, ADR-008, ADR-009. -10. **`alktype` dependency** — use `alktype` for binary layout (the - channels chunk header, future binary payload schemas) and JSON - payload schema validation (`OperationSpec`'s `input_schema`/ - `output_schema`). Do not roll your own offset map or validator. See - the alktype crate at `/workspace/@alkdev/alktype`. +10. **BAST documents for wire formats** — every binary wire format in + this crate carries a BAST (Binary Abstract Syntax Tree) document as + its machine-readable spec (e.g. the channels chunk header's + `docs/architecture/chunk-header.bast.json`, embedded as + `CHUNK_HEADER_BAST`). BAST is plain JSON — no dependency required + to author or consume it. The `alktype` crate compiles BAST into + readers/writers/validators; future codegen derives + language-specific implementations. Trivial or hot-path formats + (chunk header, tty framing) stay hand-rolled with the BAST doc as + the contract; complex formats (sftp) use the alktype engine or + codegen. Do not roll your own offset map or validator for complex + formats. See the alktype crate at `/workspace/@alkdev/alktype`. 11. **Feature flags** — transports may be feature-gated if the need arises. The base crate should compile lean (no `quinn`, no `iroh` diff --git a/Cargo.lock b/Cargo.lock index 8e1b86f..0cbd160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,6 @@ dependencies = [ name = "alkcall" version = "0.1.1" dependencies = [ - "alktype", "async-trait", "bytes", "futures", @@ -44,16 +43,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "alktype" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a102e5ec20cc0209b7c72d4abaa2c9b4f44b72a4cee2706be98d19125c2318dc" -dependencies = [ - "jsonschema", - "serde_json", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -332,12 +321,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "icu_collections" version = "2.2.0" @@ -441,16 +424,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - [[package]] name = "itoa" version = "1.0.18" @@ -756,7 +729,7 @@ dependencies = [ "ahash", "fluent-uri", "getrandom 0.3.4", - "hashbrown 0.16.1", + "hashbrown", "itoa", "micromap", "parking_lot", @@ -841,7 +814,6 @@ version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "indexmap", "itoa", "memchr", "serde", diff --git a/Cargo.toml b/Cargo.toml index d2b96c0..511065d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ name = "alkcall" default = [] [dependencies] -alktype = "0.1.0" jsonschema = { version = "0.46", default-features = false } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 688de18..ec6546d 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -272,8 +272,8 @@ feature flags) or in the downstream alknet crate. - `@alkdev/alknet: docs/architecture/` — the source architecture docs these were ported from (renumbered from alknet ADR-001..094 to alkcall ADR-001..045) -- `@alkdev/alktype` — the binary struct engine, used for channels chunk - header layout and JSON payload schema validation +- `@alkdev/alktype` — the binary struct engine; compiles BAST documents + (e.g. `chunk-header.bast.json`) into readers/writers/validators - `@alkdev/pubsub` — the TypeScript EventEnvelope prior art the call wire format was derived from diff --git a/docs/architecture/channels-wire.md b/docs/architecture/channels-wire.md index 45f9e03..e1d8bd7 100644 --- a/docs/architecture/channels-wire.md +++ b/docs/architecture/channels-wire.md @@ -21,6 +21,14 @@ multiplexing on the `BiStream` the channels layer gives it. 8 bytes of header, followed by `length` bytes of opaque payload. +The machine-readable spec is the BAST document +[`chunk-header.bast.json`](chunk-header.bast.json) (embedded in the +crate as `channels::wire::CHUNK_HEADER_BAST`). BAST is plain JSON — +any language can consume it; the `alktype` crate compiles it into +readers/writers/validators, and future codegen derives +language-specific implementations. The Rust implementation is +hand-rolled (the hot path); the BAST document is the contract. + | field | offset | width | meaning | |-------|--------|-------|---------| | `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | diff --git a/docs/architecture/chunk-header.bast.json b/docs/architecture/chunk-header.bast.json new file mode 100644 index 0000000..a99d5c6 --- /dev/null +++ b/docs/architecture/chunk-header.bast.json @@ -0,0 +1,12 @@ +{ + "$defs": { + "ChunkHeader": { + "kind": "struct", + "endian": "big", + "fields": [ + { "name": "channel_id", "kind": "uint32" }, + { "name": "length", "kind": "uint32" } + ] + } + } +} diff --git a/docs/reviews/003-alpn-prefix-rename.md b/docs/reviews/003-alpn-prefix-rename.md index 8f71bd4..9db181a 100644 --- a/docs/reviews/003-alpn-prefix-rename.md +++ b/docs/reviews/003-alpn-prefix-rename.md @@ -355,7 +355,11 @@ These were checked and are correct — the rename does not affect them: - **No logic changes:** the rename is purely mechanical. No function signatures, trait bounds, or control flow change. ✓ - **No new dependencies, no feature flag changes.** ✓ -- **`alktype` dependency unaffected.** ✓ +- **`alktype` dependency:** dropped in a follow-up commit (the only + usage, `alktype::validation::build_validator`, was a thin wrapper + over `jsonschema`; the chunk header BAST document + `docs/architecture/chunk-header.bast.json` carries the wire-format + spec without the dependency). ✓ - **483 tests pass before the rename; they should all pass after.** ✓ - **The `alknet/` prefix in `derive_alpn_from_op_name` is the only non-mechanical change** — the function's logic stays the same, only diff --git a/src/channels/wire.rs b/src/channels/wire.rs index 57ee331..6cd356d 100644 --- a/src/channels/wire.rs +++ b/src/channels/wire.rs @@ -34,6 +34,17 @@ pub const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024; /// without an explicit open op exchange. pub const CHANNEL_ID_ZERO: u32 = 0; +/// The chunk header's BAST (Binary Abstract Syntax Tree) document — +/// the machine-readable wire-format spec. The canonical copy is +/// `docs/architecture/chunk-header.bast.json`; this const embeds it so +/// downstream Rust crates can consume it without a file lookup. BAST is +/// plain JSON consumable by any language; the `alktype` crate compiles +/// it into readers/writers/validators, and future codegen derives +/// language-specific implementations from it. The hand-rolled +/// [`parse_header`]/[`write_header`] functions are the hot path; the +/// BAST document is the contract. +pub const CHUNK_HEADER_BAST: &str = include_str!("../../docs/architecture/chunk-header.bast.json"); + /// The parsed 8-byte chunk header. /// /// `length = 0` is the EOF sentinel — the reassembled stream interprets @@ -251,6 +262,39 @@ mod tests { } } + #[test] + fn chunk_header_bast_is_valid_json_and_describes_the_wire_format() { + let doc: serde_json::Value = + serde_json::from_str(CHUNK_HEADER_BAST).expect("BAST doc is valid JSON"); + let def = doc + .get("$defs") + .and_then(|d| d.get("ChunkHeader")) + .expect("ChunkHeader def present"); + assert_eq!(def.get("kind").and_then(|v| v.as_str()), Some("struct")); + assert_eq!(def.get("endian").and_then(|v| v.as_str()), Some("big")); + let fields = def + .get("fields") + .and_then(|v| v.as_array()) + .expect("fields"); + assert_eq!(fields.len(), 2); + assert_eq!( + fields[0].get("name").and_then(|v| v.as_str()), + Some("channel_id") + ); + assert_eq!( + fields[0].get("kind").and_then(|v| v.as_str()), + Some("uint32") + ); + assert_eq!( + fields[1].get("name").and_then(|v| v.as_str()), + Some("length") + ); + assert_eq!( + fields[1].get("kind").and_then(|v| v.as_str()), + Some("uint32") + ); + } + #[tokio::test] async fn read_header_round_trips_through_duplex() { let (mut reader, mut writer) = tokio::io::duplex(64); diff --git a/src/protocol/dispatch.rs b/src/protocol/dispatch.rs index 8a42403..68012f0 100644 --- a/src/protocol/dispatch.rs +++ b/src/protocol/dispatch.rs @@ -353,7 +353,7 @@ impl Dispatcher { .registry .registration(&operation_name) .and_then(|r| r.spec.publish_schema.as_ref()) - .and_then(|schema| match alktype::validation::build_validator(schema) { + .and_then(|schema| match jsonschema::options().build(schema) { Ok(v) => Some(v), Err(e) => { warn!(