jouleclaw: a routing plan carries the class of its energy figure #418
10 changed files with 161 additions and 2 deletions
1
joule-code/examples/l3-jouleclaw/Cargo.lock
generated
1
joule-code/examples/l3-jouleclaw/Cargo.lock
generated
|
|
@ -417,6 +417,7 @@ name = "jouleclaw-cascade"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jouleclaw-core",
|
"jouleclaw-core",
|
||||||
|
"jouleclaw-energy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
2
jouleclaw/jouleclaw-rs/Cargo.lock
generated
2
jouleclaw/jouleclaw-rs/Cargo.lock
generated
|
|
@ -3181,6 +3181,7 @@ name = "jouleclaw-cascade"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jouleclaw-core",
|
"jouleclaw-core",
|
||||||
|
"jouleclaw-energy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4687,6 +4688,7 @@ name = "jouleclaw-routing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jouleclaw-cascade",
|
"jouleclaw-cascade",
|
||||||
|
"jouleclaw-energy",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ description = "JouleClaw cascade dispatcher. L0 (cache) → L1 (lawful) → L2 (
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jouleclaw-core = { path = "../jouleclaw-core" }
|
jouleclaw-core = { path = "../jouleclaw-core" }
|
||||||
|
# For `Provenance` only — the family's grade vocabulary. `default-features =
|
||||||
|
# false` keeps serde out; every hardware backend is already opt-in, so this
|
||||||
|
# adds no meter to the cascade's graph.
|
||||||
|
jouleclaw-energy = { path = "../jouleclaw-energy", default-features = false }
|
||||||
|
|
||||||
# Note: pattern-lang/joule-cascade has a dev-dep on joule-l1 for integration
|
# Note: pattern-lang/joule-cascade has a dev-dep on joule-l1 for integration
|
||||||
# tests that exercise the lawful-primitive tier. Those tests don't port
|
# tests that exercise the lawful-primitive tier. Those tests don't port
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! this router doesn't know which concrete tiers exist — it asks the
|
//! this router doesn't know which concrete tiers exist — it asks the
|
||||||
//! cascade and matches by coordinate.
|
//! cascade and matches by coordinate.
|
||||||
|
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
use crate::coord::Coord;
|
use crate::coord::Coord;
|
||||||
use crate::coord_route::{CoordPredicate, SortStrategy};
|
use crate::coord_route::{CoordPredicate, SortStrategy};
|
||||||
use crate::router::{Router, RoutingPlan};
|
use crate::router::{Router, RoutingPlan};
|
||||||
|
|
@ -260,6 +261,8 @@ impl Router for CoordRouter {
|
||||||
RoutingPlan {
|
RoutingPlan {
|
||||||
tier_order,
|
tier_order,
|
||||||
router_joules: self.overhead_joules,
|
router_joules: self.overhead_joules,
|
||||||
|
// A configured constant, not a reading.
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
reasoning: format!("coord_router({}): {} tier(s) match",
|
reasoning: format!("coord_router({}): {} tier(s) match",
|
||||||
label, matching.len()),
|
label, matching.len()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
//! sub-components (R6's classifier), but those must themselves be
|
//! sub-components (R6's classifier), but those must themselves be
|
||||||
//! deterministic given fixed weights.
|
//! deterministic given fixed weights.
|
||||||
|
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
|
||||||
pub trait Router: Send {
|
pub trait Router: Send {
|
||||||
|
|
@ -63,10 +64,24 @@ pub struct RoutingPlan {
|
||||||
/// A router whose routing cost scales with its own state, or that runs a
|
/// A router whose routing cost scales with its own state, or that runs a
|
||||||
/// model to choose a route, must report what it spent rather than a
|
/// model to choose a route, must report what it spent rather than a
|
||||||
/// constant; see `jouleclaw_routing::ROUTING_JOULES` for why the flat
|
/// constant; see `jouleclaw_routing::ROUTING_JOULES` for why the flat
|
||||||
/// constant is a floor. Carrying a provenance beside this field is what
|
/// constant is a floor.
|
||||||
/// would let a verifier tell the two apart.
|
|
||||||
pub router_joules: f64,
|
pub router_joules: f64,
|
||||||
|
|
||||||
|
/// The class of [`router_joules`](Self::router_joules) — what kind of
|
||||||
|
/// evidence that number is.
|
||||||
|
///
|
||||||
|
/// Every router shipped in this workspace sets `Estimator`, because every
|
||||||
|
/// one of them charges a constant or a sum of constants. Nothing in
|
||||||
|
/// routing is measured today, and this field is what makes that legible
|
||||||
|
/// rather than something a reader has to go and discover.
|
||||||
|
///
|
||||||
|
/// It exists because the figure is **debited** (`spent += router_joules`
|
||||||
|
/// in the tier walk), so a consumer that cannot tell a measurement from a
|
||||||
|
/// declared constant cannot tell whether the budget it is enforcing means
|
||||||
|
/// anything. A router that measures its own cost sets `HwShunt` or
|
||||||
|
/// `ModelBased` here and the distinction becomes checkable.
|
||||||
|
pub router_energy_class: Provenance,
|
||||||
|
|
||||||
/// Human-readable explanation of WHY this ordering. Surfaces in
|
/// Human-readable explanation of WHY this ordering. Surfaces in
|
||||||
/// the answer's trace.
|
/// the answer's trace.
|
||||||
pub reasoning: String,
|
pub reasoning: String,
|
||||||
|
|
@ -75,10 +90,14 @@ pub struct RoutingPlan {
|
||||||
impl RoutingPlan {
|
impl RoutingPlan {
|
||||||
/// A fallback plan that says "walk everything in registration
|
/// A fallback plan that says "walk everything in registration
|
||||||
/// order." Used when the router has no opinion about the query.
|
/// order." Used when the router has no opinion about the query.
|
||||||
|
/// The figure is graded [`Provenance::Estimator`]: every caller passes a
|
||||||
|
/// constant. A router that measured its cost builds the struct directly
|
||||||
|
/// and says so.
|
||||||
pub fn fallback(router_joules: f64, reasoning: impl Into<String>) -> Self {
|
pub fn fallback(router_joules: f64, reasoning: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tier_order: Vec::new(),
|
tier_order: Vec::new(),
|
||||||
router_joules,
|
router_joules,
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
reasoning: reasoning.into(),
|
reasoning: reasoning.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ authors.workspace = true
|
||||||
description = "JouleClaw L5 learned-routing tier — episode-memory + similarity dispatch ordering. Meta-cognitive control plane, pure Rust."
|
description = "JouleClaw L5 learned-routing tier — episode-memory + similarity dispatch ordering. Meta-cognitive control plane, pure Rust."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# For `Provenance` only — the grade every routing figure now declares.
|
||||||
|
jouleclaw-energy = { path = "../jouleclaw-energy", default-features = false }
|
||||||
jouleclaw-cascade = { path = "../jouleclaw-cascade" }
|
jouleclaw-cascade = { path = "../jouleclaw-cascade" }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Token-Jaccard nearest-episode router.
|
//! Token-Jaccard nearest-episode router.
|
||||||
|
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
||||||
use jouleclaw_cascade::types::{Query, QueryInput, TierId};
|
use jouleclaw_cascade::types::{Query, QueryInput, TierId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -136,6 +137,7 @@ impl Router for LearnedRouter {
|
||||||
RoutingPlan {
|
RoutingPlan {
|
||||||
tier_order,
|
tier_order,
|
||||||
router_joules: crate::ROUTING_JOULES,
|
router_joules: crate::ROUTING_JOULES,
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
reasoning: format!(
|
reasoning: format!(
|
||||||
"L5: {} similar episodes voted; leading tier {}",
|
"L5: {} similar episodes voted; leading tier {}",
|
||||||
neighbours.len(),
|
neighbours.len(),
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,73 @@ pub use privacy::{
|
||||||
/// [`RoutingPlan`]: jouleclaw_cascade::router::RoutingPlan
|
/// [`RoutingPlan`]: jouleclaw_cascade::router::RoutingPlan
|
||||||
/// [`RoutingPlan::router_joules`]: jouleclaw_cascade::router::RoutingPlan::router_joules
|
/// [`RoutingPlan::router_joules`]: jouleclaw_cascade::router::RoutingPlan::router_joules
|
||||||
pub const ROUTING_JOULES: f64 = 8e-9;
|
pub const ROUTING_JOULES: f64 = 8e-9;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod class_tests {
|
||||||
|
use super::*;
|
||||||
|
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
||||||
|
use jouleclaw_cascade::types::{
|
||||||
|
ContextRef, JouleBudget, QualityFloor, Query, QueryInput, TierId,
|
||||||
|
};
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
|
|
||||||
|
fn q(text: &str) -> Query {
|
||||||
|
Query {
|
||||||
|
input: QueryInput::Text(text.to_string()),
|
||||||
|
budget: JouleBudget::standard(),
|
||||||
|
quality: QualityFloor::any(),
|
||||||
|
context: ContextRef::fresh(),
|
||||||
|
deadline: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Every router shipped here charges a constant, so every plan must
|
||||||
|
/// declare `Estimator`** — on the path that builds a real plan, not only
|
||||||
|
/// on the fallback.
|
||||||
|
///
|
||||||
|
/// The figure is debited (`spent += plan.router_joules` in the tier walk),
|
||||||
|
/// so a plan claiming `HwShunt` or `ModelBased` without measuring anything
|
||||||
|
/// would tell a budget it was enforcing a measured cost.
|
||||||
|
///
|
||||||
|
/// Each router is driven past its cold start with `record_outcome` and the
|
||||||
|
/// plan is asserted **not** to be a fallback first. Without that assertion
|
||||||
|
/// this test silently exercised `RoutingPlan::fallback` for both routers
|
||||||
|
/// and pinned neither of their own constructions — falsification caught
|
||||||
|
/// exactly that: mutating either literal changed nothing.
|
||||||
|
#[test]
|
||||||
|
fn every_shipped_router_declares_an_estimator_class() {
|
||||||
|
let learn = q("capital of france paris");
|
||||||
|
let ask = q("capital of france");
|
||||||
|
|
||||||
|
let mut learned = LearnedRouter::new(64, 8);
|
||||||
|
let mut phasor = PhasorRouter::with_defaults();
|
||||||
|
for _ in 0..5 {
|
||||||
|
learned.record_outcome(&learn, TierId::L0_1FactLut, 5e-6);
|
||||||
|
phasor.record_outcome(&learn, TierId::L0_1FactLut, 5e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, plan) in [
|
||||||
|
("LearnedRouter", learned.route(&ask)),
|
||||||
|
("PhasorRouter", phasor.route(&ask)),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!plan.is_fallback(),
|
||||||
|
"{name} fell back, so its own RoutingPlan construction was never reached \
|
||||||
|
and this test would pin nothing"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plan.router_energy_class,
|
||||||
|
Provenance::Estimator,
|
||||||
|
"{name} charged {} J without declaring Estimator; nothing in routing is measured",
|
||||||
|
plan.router_joules
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fallback plan is a constant too, and says so.
|
||||||
|
#[test]
|
||||||
|
fn a_fallback_plan_is_estimator_class() {
|
||||||
|
let plan = RoutingPlan::fallback(ROUTING_JOULES, "no opinion");
|
||||||
|
assert_eq!(plan.router_energy_class, Provenance::Estimator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
//! [`PhasorEmbedder`] and plugs it in; routing then votes by Hamming
|
//! [`PhasorEmbedder`] and plugs it in; routing then votes by Hamming
|
||||||
//! proximity over learned embeddings instead of token overlap.
|
//! proximity over learned embeddings instead of token overlap.
|
||||||
|
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
||||||
use jouleclaw_cascade::types::{Query, TierId};
|
use jouleclaw_cascade::types::{Query, TierId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -172,6 +173,7 @@ impl<E: PhasorEmbedder> Router for PhasorRouter<E> {
|
||||||
RoutingPlan {
|
RoutingPlan {
|
||||||
tier_order,
|
tier_order,
|
||||||
router_joules: crate::ROUTING_JOULES,
|
router_joules: crate::ROUTING_JOULES,
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
reasoning: format!("L5/phasor: {} neighbours voted", scored.len()),
|
reasoning: format!("L5/phasor: {} neighbours voted", scored.len()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
//! "walk everything" fallback, [`PrivacyRouter`] still produces a
|
//! "walk everything" fallback, [`PrivacyRouter`] still produces a
|
||||||
//! filtered ordered plan from `tier_coords`.
|
//! filtered ordered plan from `tier_coords`.
|
||||||
|
|
||||||
|
use jouleclaw_energy::Provenance;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use jouleclaw_cascade::coord::Coord;
|
use jouleclaw_cascade::coord::Coord;
|
||||||
|
|
@ -502,6 +503,9 @@ fn filter_plan(
|
||||||
RoutingPlan {
|
RoutingPlan {
|
||||||
tier_order: filtered,
|
tier_order: filtered,
|
||||||
router_joules: inner.router_joules + PRIVACY_FILTER_JOULES,
|
router_joules: inner.router_joules + PRIVACY_FILTER_JOULES,
|
||||||
|
// The sum inherits the weaker class, and the added term is a
|
||||||
|
// constant, so it is `Estimator` whatever the inner plan claimed.
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
reasoning,
|
reasoning,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -640,6 +644,55 @@ mod tests {
|
||||||
|
|
||||||
// ── PrivacyRouter ──────────────────────────────────────────────
|
// ── PrivacyRouter ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// An inner router that claims a **measured** figure, so the composition
|
||||||
|
/// rule can be tested rather than merely assumed.
|
||||||
|
struct MeasuredRouter;
|
||||||
|
impl Router for MeasuredRouter {
|
||||||
|
fn route(&self, _q: &Query) -> RoutingPlan {
|
||||||
|
RoutingPlan {
|
||||||
|
tier_order: vec![TierId::L0],
|
||||||
|
router_joules: 1.0,
|
||||||
|
reasoning: "measured".to_string(),
|
||||||
|
router_energy_class: Provenance::HwShunt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn estimate_overhead(&self, _q: &Query) -> f64 {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Adding a constant to a measured figure yields an estimate.**
|
||||||
|
///
|
||||||
|
/// `PrivacyRouter` adds `PRIVACY_FILTER_JOULES` — a constant nobody
|
||||||
|
/// measured — to whatever the inner router charged. The sum therefore
|
||||||
|
/// carries the weaker class, and since the added term is always an
|
||||||
|
/// estimate the sum is always `Estimator`, however good the inner figure
|
||||||
|
/// was.
|
||||||
|
///
|
||||||
|
/// With every shipped router on `Estimator`, taking the inner class
|
||||||
|
/// instead would read identically today — which is why falsification
|
||||||
|
/// could not tell the two apart until this test existed. An inner router
|
||||||
|
/// claiming `HwShunt` separates them.
|
||||||
|
#[test]
|
||||||
|
fn a_constant_added_to_a_measured_figure_degrades_the_class() {
|
||||||
|
let r = PrivacyRouter::new(
|
||||||
|
HeuristicPrivacyClassifier::new(),
|
||||||
|
TierSensitivityPolicy::default(),
|
||||||
|
MeasuredRouter,
|
||||||
|
);
|
||||||
|
let plan = r.route(&q("hello"));
|
||||||
|
assert!(
|
||||||
|
plan.router_joules > 1.0,
|
||||||
|
"the filter's constant must be added to the inner figure, got {}",
|
||||||
|
plan.router_joules
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plan.router_energy_class,
|
||||||
|
Provenance::Estimator,
|
||||||
|
"a sum containing an unmeasured constant claimed the inner router's measured class"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Simple inner router that always names a fixed tier order.
|
/// Simple inner router that always names a fixed tier order.
|
||||||
struct StaticRouter(Vec<TierId>);
|
struct StaticRouter(Vec<TierId>);
|
||||||
impl Router for StaticRouter {
|
impl Router for StaticRouter {
|
||||||
|
|
@ -648,6 +701,7 @@ mod tests {
|
||||||
tier_order: self.0.clone(),
|
tier_order: self.0.clone(),
|
||||||
router_joules: 0.0,
|
router_joules: 0.0,
|
||||||
reasoning: "static".to_string(),
|
reasoning: "static".to_string(),
|
||||||
|
router_energy_class: Provenance::Estimator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn estimate_overhead(&self, _q: &Query) -> f64 {
|
fn estimate_overhead(&self, _q: &Query) -> f64 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue