feat(broker): priority contexts — biases per-contexto operativo

Cierra el último pendiente de feature: el broker ahora puede operar
bajo un contexto (test/prod/foreground/secure/etc) que activa biases
declarados en las Cards.

Schema (brahman-card):
- ContextBias { pin_to: Option<String>, priority_offset: i8 }.
- Card.priority_contexts: BTreeMap<String, ContextBias>, también en
  WireCard. Las conversiones From propagan el campo.

Comportamiento (brahman-broker):
- BrokerConfig.current_context: Option<String>. Cuando es Some(ctx) y
  una Card tiene priority_contexts.get(ctx), el bias aplica:
   - Consumer-side: bias.pin_to sobreescribe Flow.pin_to estático.
   - Producer-side: bias.priority_offset se suma a la priority base
     (clamp en [Low=0, Critical=3]).
- BrokeredCard propaga priority_contexts. find_producer_for usa
  effective_priority y context_bias en lugar de comparar Priority
  directo.

Observabilidad:
- AdminConfig.current_context + StatusSnapshot.current_context.
- brahman-status imprime "Context: <nombre>" si está activo.

Wiring:
- ente-zero lee BRAHMAN_BROKER_CONTEXT del entorno y la propaga al
  broker y al admin. Sin var, biases inactivos (back-compat total).

Tests nuevos (brahman-broker, +4):
- context_priority_offset_lifts_producer_above_alphabetic_winner:
  sin contexto a-prod gana por alfabético; con context "test" b-prod
  gana por offset +1.
- context_pin_to_overrides_static_pin: static pin "real-dht", test
  override "mock-dht" → mock gana en context "test".
- unknown_context_no_op: biases declarados para "test" no aplican
  cuando broker está en "prod".
- priority_offset_clamps_to_critical: offset enorme se clampa a 3.

Validación end-to-end manual:
  $ BRAHMAN_BROKER_CONTEXT=test ente-zero &
  $ brahman-status
  Init: server=0.1.0 protocol=0.1.0 attached=true
  Context: test

Tests acumulados: 39 (card 11, broker 15, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). cargo check --workspace: 0 errores, 0
warnings.

Con esto cierran TODOS los pendientes técnicos abiertos. El único
"pendiente" que queda es el caso real para extender (priority
contexts per-deployment, scheduling biases dinámicos, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 17:46:59 +00:00
parent f19ca723b6
commit bbb9a9d2f5
7 changed files with 351 additions and 7 deletions
+42
View File
@@ -130,6 +130,13 @@ pub struct Card {
#[serde(default)]
pub genesis: Vec<Card>,
/// Biases per-contexto. La key es el nombre del contexto (p. ej.
/// `"test"`, `"prod"`, `"foreground"`). Cuando el broker está
/// configurado bajo ese contexto, el bias se aplica. Sin contexto
/// activo o sin entrada matching, este campo no afecta el ranking.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub priority_contexts: BTreeMap<String, ContextBias>,
/// Campos JSON/TOML desconocidos preservados durante I/O de archivos
/// (forward-compat). **No se transmiten por wire (postcard)** — la
/// proyección a [`WireCard`] los descarta porque `serde_json::Value`
@@ -159,6 +166,7 @@ impl Default for Card {
priority: Priority::default(),
flow: Flows::default(),
genesis: Vec::new(),
priority_contexts: BTreeMap::new(),
extensions: BTreeMap::new(),
}
}
@@ -386,6 +394,36 @@ pub enum Priority {
Critical,
}
/// Override per-contexto sobre los matches del broker.
///
/// La Card declara biases bajo `priority_contexts.<nombre>` que se
/// activan cuando el broker corre bajo ese contexto. Aplicación según rol:
///
/// - **Como consumidor**: `pin_to` sobreescribe el `pin_to` estático del
/// `Flow.pin_to` durante la búsqueda de productores.
/// - **Como productor**: `priority_offset` se suma a la priority base
/// (saturando en `[Low, Critical]`) para el ranking de candidatos.
///
/// Casos de uso típicos: test↔prod (mock vs real), foreground↔background
/// (latencia vs costo), trust gates (sólo productores con cierto nivel).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextBias {
/// Override del `pin_to` estático cuando el broker está en este
/// contexto y la Card actúa como consumidor.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pin_to: Option<String>,
/// Modifica la priority efectiva del Card como productor.
/// `+1` lo eleva, `-1` lo baja. El resultado se clampa al rango de
/// `Priority` ([Low, Critical]).
#[serde(default, skip_serializing_if = "is_zero_i8")]
pub priority_offset: i8,
}
fn is_zero_i8(v: &i8) -> bool {
*v == 0
}
// =====================================================================
// Flujos tipados (del modelo brahman)
// =====================================================================
@@ -702,6 +740,8 @@ pub struct WireCard {
pub flow: Flows,
#[serde(default)]
pub genesis: Vec<WireCard>,
#[serde(default)]
pub priority_contexts: BTreeMap<String, ContextBias>,
}
impl From<Card> for WireCard {
@@ -721,6 +761,7 @@ impl From<Card> for WireCard {
priority: c.priority,
flow: c.flow,
genesis: c.genesis.into_iter().map(WireCard::from).collect(),
priority_contexts: c.priority_contexts,
}
}
}
@@ -742,6 +783,7 @@ impl From<WireCard> for Card {
priority: w.priority,
flow: w.flow,
genesis: w.genesis.into_iter().map(Card::from).collect(),
priority_contexts: w.priority_contexts,
extensions: BTreeMap::new(),
}
}