tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198630 Definiciones

feat(core): brahman-broker — matching híbrido productor↔consumidor

Crate nuevo en crates/core/brahman-broker que indexa Cards por SessionId
y empareja flow.input de un consumidor con flow.output del productor más
adecuado.

Tres ejes de matching:

1. Estrategia (MatchStrategy):
   - Exact: igualdad estricta de TypeRef.
   - Structural: misma forma — para Wit, mismo package + name (ignora
     interface); para Primitive, mismo name.
   - ExactThenStructural (default): prefiere Exact; cae en Structural si
     no hay. Reporta cuál ganó en Match.via.

2. Override pin_to: si el consumidor declara pin_to = "label", el broker
   prefiere productores con ese label (siempre que el tipo matchee).
   Si la pista falla, cae en type-search general. Match.pinned indica
   qué camino se siguió.

3. Prioridad: empate de tipo se resuelve por Card.priority (Critical >
   High > Normal > Low). Empate de prioridad se resuelve lexicográfica-
   mente por label (estable y determinista).

API mínima:
- Broker::new(config)
- register(session, &Card) / unregister(session)
- find_producer_for(consumer_session, input_name) -> Option<Match>
- all_matches() -> Vec<Match> (introspección)

El broker es stateless w.r.t. routes: cada query se computa bajo demanda.
Sólo persiste el índice de BrokeredCard (vista mínima: label, priority,
inputs, outputs).

Cambio aditivo en brahman-card: Priority deriva PartialOrd/Ord/Hash para
ser usable como tiebreaker.

Tests: 11/11.
- exact_match_same_typeref
- structural_ignores_interface
- exact_strategy_rejects_interface_mismatch
- exact_then_structural_prefers_exact
- pin_to_overrides_type_search
- pin_to_unresolvable_falls_back_to_type_match
- priority_breaks_ties
- label_alpha_breaks_priority_ties
- unregister_removes_producer
- no_self_loops
- all_matches_lists_pairs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

5091106c2177cd4d337fb5f4e9b93eeb15daa4ae · por Sergio · (hace 5 meses) · padre 814390fee · ver el árbol en este commit

Delta semántico · 36

crates/core/brahman-broker/src/lib.rs

5 archivos · +730 −2

diff --git a/Cargo.lock b/Cargo.lock
index 3e20a2e..3ab86cf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1139,6 +1139,16 @@ dependencies = [
  "syn 2.0.117",
 ]
 
+[[package]]
+name = "brahman-broker"
+version = "0.1.0"
+dependencies = [
+ "brahman-card",
+ "serde",
+ "thiserror 2.0.18",
+ "ulid",
+]
+
 [[package]]
 name = "brahman-card"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 94bc8e9..4907797 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
     # ============================================================
     "crates/core/brahman-card",
     "crates/core/brahman-handshake",
+    "crates/core/brahman-broker",
     "crates/core/ente-card",
     "crates/core/ente-bus",
     "crates/core/ente-cas",
diff --git a/crates/core/brahman-broker/Cargo.toml b/crates/core/brahman-broker/Cargo.toml
new file mode 100644
index 0000000..7d33864
--- /dev/null
+++ b/crates/core/brahman-broker/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "brahman-broker"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "Brahman — broker de tipos: empareja productores↔consumidores por TypeRef con matching híbrido (exact + structural) y prioridad."
+
+[dependencies]
+brahman-card = { path = "../brahman-card" }
+serde = { workspace = true }
+thiserror = { workspace = true }
+ulid = { workspace = true }
diff --git a/crates/core/brahman-broker/src/lib.rs b/crates/core/brahman-broker/src/lib.rs
new file mode 100644
index 0000000..3a7ff4b
--- /dev/null
+++ b/crates/core/brahman-broker/src/lib.rs
@@ -0,0 +1,701 @@
+//! `brahman-broker` — empareja productores y consumidores por tipo de flujo.
+//!
+//! El broker indexa [`brahman_card::Card`]s registradas por `SessionId` y,
+//! para cada `flow.input` de un consumidor, busca el `flow.output`
+//! compatible de mejor calidad entre los demás. Tres ejes:
+//!
+//! 1. **Estrategia de matching** ([`MatchStrategy`]):
+//!    - `Exact`: igualdad estricta de [`brahman_card::TypeRef`].
+//!    - `Structural`: misma forma (mismo `package` + `name` para Wit;
+//!      ignora `interface`).
+//!    - `ExactThenStructural`: prefiere exact; cae en structural si no hay.
+//!
+//! 2. **Override `pin_to`**: si el consumidor declara `pin_to = "label"`,
+//!    el broker prefiere productores cuya Card tenga ese `label` (siempre
+//!    que el tipo siga matcheando). Si la pista no resuelve, cae en
+//!    matching por tipo normal.
+//!
+//! 3. **Prioridad**: empate de tipo se resuelve por
+//!    [`brahman_card::Priority`] del productor (mayor gana). Empate de
+//!    prioridad se resuelve lexicográficamente por `label` (estable y
+//!    determinista).
+//!
+//! El broker es **stateless w.r.t. routes**: cada `find_producer_for` o
+//! `all_matches` se calcula bajo demanda. La única persistencia es el
+//! índice de Cards registradas. Esto permite re-evaluar matches cuando
+//! cambia el set sin invalidar caches.
+
+#![forbid(unsafe_code)]
+#![warn(rust_2018_idioms)]
+
+use std::collections::BTreeMap;
+
+use brahman_card::{Card, Flow, Priority, TypeRef};
+use serde::{Deserialize, Serialize};
+use ulid::Ulid;
+
+/// Identificador de sesión emitido por el handshake. Idéntico al usado por
+/// `brahman-handshake` (no es un re-export para evitar la dependencia).
+pub type SessionId = Ulid;
+
+/// Estrategia de matching de tipos.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum MatchStrategy {
+    /// Igualdad estricta de `TypeRef`.
+    Exact,
+    /// Misma forma: para `Wit`, mismo `package` + `name`; para
+    /// `Primitive`, mismo `name`.
+    Structural,
+    /// Híbrido: intenta `Exact` primero; si no matchea, `Structural`.
+    /// Reporta cuál estrategia ganó en [`Match::via`].
+    #[default]
+    ExactThenStructural,
+}
+
+/// Configuración del broker.
+#[derive(Debug, Clone, Default)]
+pub struct BrokerConfig {
+    pub strategy: MatchStrategy,
+}
+
+/// Vista mínima de una Card que el broker necesita.
+#[derive(Debug, Clone)]
+pub struct BrokeredCard {
+    pub session: SessionId,
+    pub label: String,
+    pub priority: Priority,
+    pub inputs: Vec<Flow>,
+    pub outputs: Vec<Flow>,
+}
+
+impl BrokeredCard {
+    fn from_card(session: SessionId, card: &Card) -> Self {
+        Self {
+            session,
+            label: card.label.clone(),
+            priority: card.priority,
+            inputs: card.flow.input.clone(),
+            outputs: card.flow.output.clone(),
+        }
+    }
+}
+
+/// Punto extremo de un flujo: qué sesión + nombre del flow dentro de su Card.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct Endpoint {
+    pub session: SessionId,
+    pub flow_name: String,
+}
+
+/// Match concreto entre un consumidor y un productor.
+#[derive(Debug, Clone)]
+pub struct Match {
+    pub consumer: Endpoint,
+    pub consumer_label: String,
+    pub producer: Endpoint,
+    pub producer_label: String,
+    /// Tipo del flow (lado consumidor — lado productor coincide en
+    /// estrategia Exact, puede diferir en `interface` en Structural).
+    pub ty: TypeRef,
+    /// Estrategia que efectivamente matcheó.
+    pub via: MatchStrategy,
+    /// `true` si el match fue resuelto por `pin_to` y no por type-search.
+    pub pinned: bool,
+}
+
+// =====================================================================
+// Broker
+// =====================================================================
+
+/// El broker. Registra Cards por SessionId, computa matches bajo demanda.
+#[derive(Debug, Clone, Default)]
+pub struct Broker {
+    cards: BTreeMap<SessionId, BrokeredCard>,
+    config: BrokerConfig,
+}
+
+impl Broker {
+    pub fn new(config: BrokerConfig) -> Self {
+        Self {
+            cards: BTreeMap::new(),
+            config,
+        }
+    }
+
+    /// Registra una Card. Devuelve `Some(prev)` si reemplazó una existente.
+    pub fn register(&mut self, session: SessionId, card: &Card) -> Option<BrokeredCard> {
+        self.cards.insert(session, BrokeredCard::from_card(session, card))
+    }
+
+    /// Quita una Card por sesión.
+    pub fn unregister(&mut self, session: SessionId) -> Option<BrokeredCard> {
+        self.cards.remove(&session)
+    }
+
+    /// Cardinalidad del registro.
+    pub fn len(&self) -> usize {
+        self.cards.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.cards.is_empty()
+    }
+
+    /// Iterador sobre las sesiones registradas.
+    pub fn sessions(&self) -> impl Iterator<Item = SessionId> + '_ {
+        self.cards.keys().copied()
+    }
+
+    /// Busca el mejor productor para un input específico de un consumidor.
+    ///
+    /// Algoritmo:
+    /// 1. Resuelve el flow input en el consumidor.
+    /// 2. Si tiene `pin_to`, prefiere productores con ese `label` que
+    ///    matcheen el tipo (cualquier estrategia configurada).
+    /// 3. Si no hay pin_to o la pista falló, escanea todos los outputs
+    ///    de las otras Cards. Filtra por compatibilidad de tipo.
+    /// 4. Ordena por (priority desc, label asc) y devuelve el primero.
+    pub fn find_producer_for(&self, consumer: SessionId, input_name: &str) -> Option<Match> {
+        let cons = self.cards.get(&consumer)?;
+        let input = cons.inputs.iter().find(|f| f.name == input_name)?;
+
+        // pin_to: short-circuit. Si la pista resuelve, gana.
+        if let Some(pin) = &input.pin_to {
+            for prod in self.cards.values() {
+                if prod.session == consumer || &prod.label != pin {
+                    continue;
+                }
+                for out in &prod.outputs {
+                    if let Some(via) = self.types_match(&input.ty, &out.ty) {
+                        return Some(self.make_match(cons, prod, input, out, via, true));
+                    }
+                }
+            }
+            // Fall through: pin no resuelto, type-search general.
+        }
+
+        let mut candidates: Vec<(&BrokeredCard, &Flow, MatchStrategy)> = Vec::new();
+        for prod in self.cards.values() {
+            if prod.session == consumer {
+                continue;
+            }
+            for out in &prod.outputs {
+                if let Some(via) = self.types_match(&input.ty, &out.ty) {
+                    candidates.push((prod, out, via));
+                }
+            }
+        }
+
+        candidates.sort_by(|(a, _, _), (b, _, _)| {
+            b.priority
+                .cmp(&a.priority)
+                .then_with(|| a.label.cmp(&b.label))
+        });
+
+        let (prod, out, via) = candidates.into_iter().next()?;
+        Some(self.make_match(cons, prod, input, out, via, false))
+    }
+
+    /// Calcula todos los matches consumer→producer en el set actual.
+    /// Útil para introspección o para que el Admin emita rutas en lote.
+    pub fn all_matches(&self) -> Vec<Match> {
+        let mut out = Vec::new();
+        for cons in self.cards.values() {
+            for input in &cons.inputs {
+                if let Some(m) = self.find_producer_for(cons.session, &input.name) {
+                    out.push(m);
+                }
+            }
+        }
+        out
+    }
+
+    fn types_match(&self, consumer_ty: &TypeRef, producer_ty: &TypeRef) -> Option<MatchStrategy> {
+        match self.config.strategy {
+            MatchStrategy::Exact => exact_match(consumer_ty, producer_ty).then_some(MatchStrategy::Exact),
+            MatchStrategy::Structural => {
+                structural_match(consumer_ty, producer_ty).then_some(MatchStrategy::Structural)
+            }
+            MatchStrategy::ExactThenStructural => {
+                if exact_match(consumer_ty, producer_ty) {
+                    Some(MatchStrategy::Exact)
+                } else if structural_match(consumer_ty, producer_ty) {
+                    Some(MatchStrategy::Structural)
+                } else {
+                    None
+                }
+            }
+        }
+    }
+
+    fn make_match(
+        &self,
+        cons: &BrokeredCard,
+        prod: &BrokeredCard,
+        input: &Flow,
+        output: &Flow,
+        via: MatchStrategy,
+        pinned: bool,
+    ) -> Match {
+        Match {
+            consumer: Endpoint {
+                session: cons.session,
+                flow_name: input.name.clone(),
+            },
+            consumer_label: cons.label.clone(),
+            producer: Endpoint {
+                session: prod.session,
+                flow_name: output.name.clone(),
+            },
+            producer_label: prod.label.clone(),
+            ty: input.ty.clone(),
+            via,
+            pinned,
+        }
+    }
+}
+
+// =====================================================================
+// Predicados de matching (libres, testeables aislados)
+// =====================================================================
+
+fn exact_match(a: &TypeRef, b: &TypeRef) -> bool {
+    a == b
+}
+
+fn structural_match(a: &TypeRef, b: &TypeRef) -> bool {
+    match (a, b) {
+        (TypeRef::Primitive { name: na }, TypeRef::Primitive { name: nb }) => na == nb,
+        (
+            TypeRef::Wit {
+                package: pa, name: na, ..
+            },
+            TypeRef::Wit {
+                package: pb, name: nb, ..
+            },
+        ) => pa == pb && na == nb,
+        _ => false,
+    }
+}
+
+// =====================================================================
+// Tests
+// =====================================================================
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use brahman_card::{Card, Flows, Payload, Supervision, CARD_SCHEMA_VERSION};
+
+    fn card(label: &str, priority: Priority, flows: Flows) -> Card {
+        Card {
+            schema_version: CARD_SCHEMA_VERSION,
+            id: Ulid::new(),
+            label: label.into(),
+            payload: Payload::Virtual,
+            supervision: Supervision::OneShot,
+            priority,
+            flow: flows,
+            ..Default::default()
+        }
+    }
+
+    fn prim(name: &str) -> TypeRef {
+        TypeRef::Primitive { name: name.into() }
+    }
+
+    fn wit(pkg: &str, iface: Option<&str>, name: &str) -> TypeRef {
+        TypeRef::Wit {
+            package: pkg.into(),
+            interface: iface.map(|s| s.into()),
+            name: name.into(),
+        }
+    }
+
+    fn flow(name: &str, ty: TypeRef, pin: Option<&str>) -> Flow {
+        Flow {
+            name: name.into(),
+            ty,
+            pin_to: pin.map(|s| s.into()),
+        }
+    }
+
+    #[test]
+    fn exact_match_same_typeref() {
+        let mut b = Broker::new(BrokerConfig {
+            strategy: MatchStrategy::Exact,
+        });
+        let producer = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("results", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("query", prim("string"), None)],
+                output: vec![],
+            },
+        );
+        let s_prod = Ulid::new();
+        let s_cons = Ulid::new();
+        b.register(s_prod, &producer);
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "query").expect("match");
+        assert_eq!(m.producer_label, "dht");
+        assert_eq!(m.via, MatchStrategy::Exact);
+        assert!(!m.pinned);
+    }
+
+    #[test]
+    fn structural_ignores_interface() {
+        let mut b = Broker::new(BrokerConfig {
+            strategy: MatchStrategy::Structural,
+        });
+        let producer = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow(
+                    "out",
+                    wit("brahman:dht", Some("v1"), "entity-result"),
+                    None,
+                )],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow(
+                    "in",
+                    wit("brahman:dht", Some("v2"), "entity-result"),
+                    None,
+                )],
+                output: vec![],
+            },
+        );
+        let s_prod = Ulid::new();
+        let s_cons = Ulid::new();
+        b.register(s_prod, &producer);
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        assert_eq!(m.via, MatchStrategy::Structural);
+    }
+
+    #[test]
+    fn exact_strategy_rejects_interface_mismatch() {
+        let mut b = Broker::new(BrokerConfig {
+            strategy: MatchStrategy::Exact,
+        });
+        let producer = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow(
+                    "out",
+                    wit("brahman:dht", Some("v1"), "entity-result"),
+                    None,
+                )],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow(
+                    "in",
+                    wit("brahman:dht", Some("v2"), "entity-result"),
+                    None,
+                )],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &producer);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        assert!(b.find_producer_for(s_cons, "in").is_none());
+    }
+
+    #[test]
+    fn exact_then_structural_prefers_exact() {
+        let mut b = Broker::new(BrokerConfig {
+            strategy: MatchStrategy::ExactThenStructural,
+        });
+        // Productor 1: match estructural (interface diferente)
+        let p_struct = card(
+            "dht-cache",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow(
+                    "out",
+                    wit("brahman:dht", Some("v2"), "entity-result"),
+                    None,
+                )],
+            },
+        );
+        // Productor 2: match exact (interface igual)
+        let p_exact = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow(
+                    "out",
+                    wit("brahman:dht", Some("v1"), "entity-result"),
+                    None,
+                )],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow(
+                    "in",
+                    wit("brahman:dht", Some("v1"), "entity-result"),
+                    None,
+                )],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &p_struct);
+        b.register(Ulid::new(), &p_exact);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        // El exact gana incluso si tiene priority igual: por estrategia.
+        assert_eq!(m.producer_label, "dht");
+        assert_eq!(m.via, MatchStrategy::Exact);
+    }
+
+    #[test]
+    fn pin_to_overrides_type_search() {
+        let mut b = Broker::new(BrokerConfig::default());
+        // Dos productores que producen el mismo tipo.
+        let p1 = card(
+            "dht-prod",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let p2 = card(
+            "dht-test",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), Some("dht-test"))],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &p1);
+        b.register(Ulid::new(), &p2);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        assert_eq!(m.producer_label, "dht-test");
+        assert!(m.pinned);
+    }
+
+    #[test]
+    fn pin_to_unresolvable_falls_back_to_type_match() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let p = card(
+            "real-dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), Some("nonexistent"))],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &p);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        assert_eq!(m.producer_label, "real-dht");
+        assert!(!m.pinned);
+    }
+
+    #[test]
+    fn priority_breaks_ties() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let p_low = card(
+            "z-dht",
+            Priority::Low,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let p_high = card(
+            "a-dht",
+            Priority::High,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), None)],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &p_low);
+        b.register(Ulid::new(), &p_high);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        assert_eq!(m.producer_label, "a-dht"); // priority High > Low
+    }
+
+    #[test]
+    fn label_alpha_breaks_priority_ties() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let p1 = card(
+            "z-dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let p2 = card(
+            "a-dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), None)],
+                output: vec![],
+            },
+        );
+        b.register(Ulid::new(), &p1);
+        b.register(Ulid::new(), &p2);
+        let s_cons = Ulid::new();
+        b.register(s_cons, &consumer);
+
+        let m = b.find_producer_for(s_cons, "in").expect("match");
+        assert_eq!(m.producer_label, "a-dht"); // alfabético gana
+    }
+
+    #[test]
+    fn unregister_removes_producer() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let p = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let consumer = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), None)],
+                output: vec![],
+            },
+        );
+        let s_p = Ulid::new();
+        b.register(s_p, &p);
+        let s_c = Ulid::new();
+        b.register(s_c, &consumer);
+
+        assert!(b.find_producer_for(s_c, "in").is_some());
+        b.unregister(s_p);
+        assert!(b.find_producer_for(s_c, "in").is_none());
+    }
+
+    #[test]
+    fn no_self_loops() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let same = card(
+            "echo",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("in", prim("string"), None)],
+                output: vec![flow("out", prim("string"), None)],
+            },
+        );
+        let s = Ulid::new();
+        b.register(s, &same);
+
+        // Solo una Card registrada — no hay otra que produzca string.
+        assert!(b.find_producer_for(s, "in").is_none());
+    }
+
+    #[test]
+    fn all_matches_lists_pairs() {
+        let mut b = Broker::new(BrokerConfig::default());
+        let dht = card(
+            "dht",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("query", prim("string"), None)],
+                output: vec![flow("results", prim("bytes"), None)],
+            },
+        );
+        let ui = card(
+            "ui",
+            Priority::Normal,
+            Flows {
+                input: vec![flow("data", prim("bytes"), None)],
+                output: vec![flow("user-input", prim("string"), None)],
+            },
+        );
+        b.register(Ulid::new(), &dht);
+        b.register(Ulid::new(), &ui);
+
+        let matches = b.all_matches();
+        assert_eq!(matches.len(), 2);
+        // dht.query ← ui.user-input  y  ui.data ← dht.results
+        let pairs: Vec<_> = matches
+            .iter()
+            .map(|m| (m.consumer_label.as_str(), m.producer_label.as_str()))
+            .collect();
+        assert!(pairs.contains(&("dht", "ui")));
+        assert!(pairs.contains(&("ui", "dht")));
+    }
+}
diff --git a/crates/core/brahman-card/src/lib.rs b/crates/core/brahman-card/src/lib.rs
index 8c7d40e..de43442 100644
--- a/crates/core/brahman-card/src/lib.rs
+++ b/crates/core/brahman-card/src/lib.rs
@@ -360,8 +360,9 @@ pub enum Lifecycle {
     Widget,
 }
 
-/// Prioridad de scheduling.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
+/// Prioridad de scheduling. Orden: `Low < Normal < High < Critical` —
+/// usable como tiebreaker en el broker (mayor priority gana).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase")]
 pub enum Priority {
     Low,