feat(core): brahman-card-wit — extractor opcional de contratos WIT
Crate nuevo crates/core/brahman-card-wit que parsea texto WIT con
wit-parser y devuelve un Vec<WitInterface> (de brahman-card),
listo para acoplarse a una ResolvedCard::from_conscious(card, wit).
Ámbito intencional: sólo parsing texto, no toca wasm-tools ni
wit-component. Es opt-in: brahman-card no depende de éste.
API pública:
- parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError>
- parse_wit_file(path: impl AsRef<Path>) -> Result<...>
Cada WitInterface incluye: package, world, imports, exports.
Las interfaces importadas/exportadas (no sólo funciones) se
resuelven por nombre via resolve.interfaces[id].name; las
funciones inline aparecen como WorldKey::Name directo.
Example CLI: brahman-wit-info <ruta.wit> imprime los worlds.
$ brahman-wit-info shared_wit/protocol.wit
2 world(s):
package: brahman:protocol@0.1.0
world: module
imports: types, handshake, lifecycle
exports: run
...
Tests: 4/4 (inline + archivo real + parse error + world vacío).
Workspace: 0 errores.
CHANGELOG.md actualizado con la entrada nueva y la del commit
anterior (7b589b8) que faltaba.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f4dc01900492ebebac247d2d56988db535784bcf · por Sergio · 2026-05-08T17:05:17Z (hace 5 meses) · padre 7b589b863 · ver el árbol en este commit
Delta semántico · 12
crates/core/brahman-card-wit/examples/brahman-wit-info.rs
crates/core/brahman-card-wit/src/lib.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a5c165..579033b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,25 @@ ratio/diff ver `git show <sha>`.
## 2026-05-08
+### feat(core): brahman-card-wit — extractor opcional de contratos WIT
+- Crate nuevo `crates/core/brahman-card-wit` con `wit-parser = "0.230"`.
+- API: `parse_wit(source)` y `parse_wit_file(path)` devuelven
+ `Vec<WitInterface>` (uno por `world` declarado).
+- Interfaces importadas/exportadas (no sólo funciones) se resuelven
+ por nombre via `resolve.interfaces[id].name`.
+- Example `crates/core/brahman-card-wit/examples/brahman-wit-info.rs`
+ CLI: `brahman-wit-info shared_wit/protocol.wit` → lista paquete,
+ worlds, imports y exports.
+- 4 tests: inline, archivo real (`shared_wit/protocol.wit`), parse
+ error, world vacío.
+- Validado contra `protocol.wit`: detecta worlds `module` y
+ `admin-host` con sus imports/exports correctos.
+
+### `7b589b8` chore: agrega CHANGELOG.md retroactivo
+- `CHANGELOG.md` en la raíz con los 11 commits previos documentados
+ acción por acción. A partir de este punto, cada cambio sustantivo
+ actualiza también este archivo en el mismo commit.
+
### `8a83a26` feat(handshake): notificación push de matches
- Frame `MatchEvent { kind: Available | Lost, ... }` añadido al protocolo.
- `Session::run_post_handshake` usa `tokio::select!` para multiplexar
diff --git a/Cargo.lock b/Cargo.lock
index a8a61bd..33cd755 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1176,6 +1176,16 @@ dependencies = [
"ulid",
]
+[[package]]
+name = "brahman-card-wit"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "brahman-card",
+ "thiserror 2.0.18",
+ "wit-parser 0.230.0",
+]
+
[[package]]
name = "brahman-handshake"
version = "0.1.0"
@@ -10277,6 +10287,17 @@ dependencies = [
"indexmap 2.14.0",
]
+[[package]]
+name = "wasmparser"
+version = "0.230.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed"
+dependencies = [
+ "bitflags 2.11.1",
+ "indexmap 2.14.0",
+ "semver",
+]
+
[[package]]
name = "wasmparser"
version = "0.244.0"
@@ -11123,7 +11144,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck 0.5.0",
- "wit-parser",
+ "wit-parser 0.244.0",
]
[[package]]
@@ -11173,7 +11194,25 @@ dependencies = [
"wasm-encoder 0.244.0",
"wasm-metadata",
"wasmparser 0.244.0",
- "wit-parser",
+ "wit-parser 0.244.0",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.230.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "679fde5556495f98079a8e6b9ef8c887f731addaffa3d48194075c1dd5cd611b"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.14.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.230.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 2be928c..a894516 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ members = [
# core/ — Init y compat (arje absorbido)
# ============================================================
"crates/core/brahman-card",
+ "crates/core/brahman-card-wit",
"crates/core/brahman-handshake",
"crates/core/brahman-broker",
"crates/core/brahman-admin",
diff --git a/crates/core/brahman-card-wit/Cargo.toml b/crates/core/brahman-card-wit/Cargo.toml
new file mode 100644
index 0000000..f57bcf4
--- /dev/null
+++ b/crates/core/brahman-card-wit/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "brahman-card-wit"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "Brahman — extractor opcional: parsea contratos WIT y devuelve `WitInterface` listo para acoplar a una `Card`."
+
+[dependencies]
+brahman-card = { path = "../brahman-card" }
+wit-parser = "0.230"
+thiserror = { workspace = true }
+
+[dev-dependencies]
+anyhow = { workspace = true }
+
+[[example]]
+name = "brahman-wit-info"
+path = "examples/brahman-wit-info.rs"
diff --git a/crates/core/brahman-card-wit/examples/brahman-wit-info.rs b/crates/core/brahman-card-wit/examples/brahman-wit-info.rs
new file mode 100644
index 0000000..bb8910f
--- /dev/null
+++ b/crates/core/brahman-card-wit/examples/brahman-wit-info.rs
@@ -0,0 +1,45 @@
+//! `brahman-wit-info` — inspecciona un archivo WIT y lista sus worlds.
+//!
+//! Uso:
+//! ```sh
+//! cargo run -p brahman-card-wit --example brahman-wit-info -- shared_wit/protocol.wit
+//! ```
+
+use std::process::ExitCode;
+
+fn main() -> ExitCode {
+ let path = match std::env::args().nth(1) {
+ Some(p) => p,
+ None => {
+ eprintln!("uso: brahman-wit-info <ruta.wit>");
+ return ExitCode::from(2);
+ }
+ };
+
+ let worlds = match brahman_card_wit::parse_wit_file(&path) {
+ Ok(w) => w,
+ Err(e) => {
+ eprintln!("error parseando {path}: {e}");
+ return ExitCode::from(1);
+ }
+ };
+
+ if worlds.is_empty() {
+ println!("(ningún world declarado)");
+ return ExitCode::SUCCESS;
+ }
+
+ println!("{} world(s):", worlds.len());
+ for w in &worlds {
+ println!();
+ println!(" package: {}", w.package);
+ println!(" world: {}", w.world);
+ if !w.imports.is_empty() {
+ println!(" imports: {}", w.imports.join(", "));
+ }
+ if !w.exports.is_empty() {
+ println!(" exports: {}", w.exports.join(", "));
+ }
+ }
+ ExitCode::SUCCESS
+}
diff --git a/crates/core/brahman-card-wit/src/lib.rs b/crates/core/brahman-card-wit/src/lib.rs
new file mode 100644
index 0000000..291b415
--- /dev/null
+++ b/crates/core/brahman-card-wit/src/lib.rs
@@ -0,0 +1,165 @@
+//! `brahman-card-wit` — extractor de contratos WIT.
+//!
+//! Crate **opcional** (no es dep de `brahman-card`). Parsea texto WIT
+//! mediante [`wit-parser`] y devuelve una lista de [`WitInterface`]
+//! (uno por `world`) lista para acoplarse a una [`brahman_card::Card`]
+//! cuando se construye una [`brahman_card::ResolvedCard`].
+//!
+//! Casos de uso:
+//!
+//! - El Init lee `<modulo>/wit/protocol.wit` durante el descubrimiento
+//! y lo combina con la Card del módulo para obtener una
+//! `ResolvedCard::from_conscious(card, wit)`.
+//! - Tooling (`brahman-wit-info`) inspecciona un `.wit` y muestra
+//! sus mundos, exports e imports.
+//!
+//! No depende de `wasm-tools`/`wit-component` — sólo del parser texto.
+
+#![forbid(unsafe_code)]
+#![warn(rust_2018_idioms)]
+
+use std::path::{Path, PathBuf};
+
+use brahman_card::WitInterface;
+use thiserror::Error;
+use wit_parser::{Resolve, WorldKey};
+
+#[derive(Debug, Error)]
+pub enum WitError {
+ #[error("parse: {0}")]
+ Parse(String),
+ #[error("E/S: {0}")]
+ Io(#[from] std::io::Error),
+}
+
+/// Parsea WIT desde una string. Devuelve un `WitInterface` por cada
+/// `world` declarado.
+pub fn parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError> {
+ parse_with_path(source, Path::new("inline.wit"))
+}
+
+/// Parsea WIT desde un archivo. Útil para `module/wit/protocol.wit`.
+pub fn parse_wit_file(path: impl AsRef<Path>) -> Result<Vec<WitInterface>, WitError> {
+ let p = path.as_ref();
+ let source = std::fs::read_to_string(p)?;
+ parse_with_path(&source, p)
+}
+
+fn parse_with_path(source: &str, path: &Path) -> Result<Vec<WitInterface>, WitError> {
+ let mut resolve = Resolve::new();
+ let path_buf: PathBuf = path.to_path_buf();
+ resolve
+ .push_str(&path_buf, source)
+ .map_err(|e| WitError::Parse(e.to_string()))?;
+
+ let mut out = Vec::new();
+ for (_pkg_id, pkg) in resolve.packages.iter() {
+ let pkg_name = pkg.name.to_string();
+ for (_name, &world_id) in &pkg.worlds {
+ let world = &resolve.worlds[world_id];
+ let exports = collect_keys(world.exports.iter().map(|(k, _)| k), &resolve);
+ let imports = collect_keys(world.imports.iter().map(|(k, _)| k), &resolve);
+ out.push(WitInterface {
+ package: pkg_name.clone(),
+ world: world.name.clone(),
+ exports,
+ imports,
+ });
+ }
+ }
+ Ok(out)
+}
+
+fn collect_keys<'a, I>(keys: I, resolve: &Resolve) -> Vec<String>
+where
+ I: Iterator<Item = &'a WorldKey>,
+{
+ keys.map(|k| match k {
+ WorldKey::Name(n) => n.clone(),
+ WorldKey::Interface(id) => resolve.interfaces[*id]
+ .name
+ .clone()
+ .unwrap_or_else(|| format!("<interface#{}>", id.index())),
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const SAMPLE: &str = r#"
+package brahman:test@0.1.0;
+
+interface handshake {
+ hello: func() -> result<_, string>;
+}
+
+interface lifecycle {
+ report: func();
+}
+
+world module {
+ import handshake;
+ import lifecycle;
+ export run: func() -> result<_, string>;
+}
+"#;
+
+ #[test]
+ fn parses_inline_wit() {
+ let worlds = parse_wit(SAMPLE).unwrap();
+ assert_eq!(worlds.len(), 1, "esperaba un único world");
+ let w = &worlds[0];
+ assert!(w.package.starts_with("brahman:test"));
+ assert_eq!(w.world, "module");
+ assert!(
+ w.imports.iter().any(|i| i == "handshake"),
+ "imports={:?}",
+ w.imports
+ );
+ assert!(
+ w.imports.iter().any(|i| i == "lifecycle"),
+ "imports={:?}",
+ w.imports
+ );
+ assert!(
+ w.exports.iter().any(|e| e == "run"),
+ "exports={:?}",
+ w.exports
+ );
+ }
+
+ #[test]
+ fn parses_shared_protocol() {
+ let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../shared_wit/protocol.wit");
+ let worlds = parse_wit_file(path).unwrap();
+ assert!(
+ worlds.iter().any(|w| w.world == "module"),
+ "no encontró world 'module' en {:?}",
+ worlds.iter().map(|w| &w.world).collect::<Vec<_>>()
+ );
+ assert!(
+ worlds.iter().any(|w| w.world == "admin-host"),
+ "no encontró world 'admin-host'"
+ );
+ }
+
+ #[test]
+ fn parse_error_on_garbage() {
+ let bad = "this is not wit at all { } } ;;;;";
+ assert!(matches!(parse_wit(bad), Err(WitError::Parse(_))));
+ }
+
+ #[test]
+ fn empty_world_handled() {
+ let src = r#"
+package brahman:empty@0.1.0;
+world hollow {}
+"#;
+ let worlds = parse_wit(src).unwrap();
+ assert_eq!(worlds.len(), 1);
+ assert!(worlds[0].exports.is_empty());
+ assert!(worlds[0].imports.is_empty());
+ }
+}