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"
|
||||
dependencies = [
|
||||
"jouleclaw-core",
|
||||
"jouleclaw-energy",
|
||||
]
|
||||
|
||||
[[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"
|
||||
dependencies = [
|
||||
"jouleclaw-core",
|
||||
"jouleclaw-energy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4687,6 +4688,7 @@ name = "jouleclaw-routing"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"jouleclaw-cascade",
|
||||
"jouleclaw-energy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ description = "JouleClaw cascade dispatcher. L0 (cache) → L1 (lawful) → L2 (
|
|||
|
||||
[dependencies]
|
||||
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
|
||||
# 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
|
||||
//! cascade and matches by coordinate.
|
||||
|
||||
use jouleclaw_energy::Provenance;
|
||||
use crate::coord::Coord;
|
||||
use crate::coord_route::{CoordPredicate, SortStrategy};
|
||||
use crate::router::{Router, RoutingPlan};
|
||||
|
|
@ -260,6 +261,8 @@ impl Router for CoordRouter {
|
|||
RoutingPlan {
|
||||
tier_order,
|
||||
router_joules: self.overhead_joules,
|
||||
// A configured constant, not a reading.
|
||||
router_energy_class: Provenance::Estimator,
|
||||
reasoning: format!("coord_router({}): {} tier(s) match",
|
||||
label, matching.len()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
//! sub-components (R6's classifier), but those must themselves be
|
||||
//! deterministic given fixed weights.
|
||||
|
||||
use jouleclaw_energy::Provenance;
|
||||
use crate::types::*;
|
||||
|
||||
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
|
||||
/// model to choose a route, must report what it spent rather than a
|
||||
/// constant; see `jouleclaw_routing::ROUTING_JOULES` for why the flat
|
||||
/// constant is a floor. Carrying a provenance beside this field is what
|
||||
/// would let a verifier tell the two apart.
|
||||
/// constant is a floor.
|
||||
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
|
||||
/// the answer's trace.
|
||||
pub reasoning: String,
|
||||
|
|
@ -75,10 +90,14 @@ pub struct RoutingPlan {
|
|||
impl RoutingPlan {
|
||||
/// A fallback plan that says "walk everything in registration
|
||||
/// 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 {
|
||||
Self {
|
||||
tier_order: Vec::new(),
|
||||
router_joules,
|
||||
router_energy_class: Provenance::Estimator,
|
||||
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."
|
||||
|
||||
[dependencies]
|
||||
# For `Provenance` only — the grade every routing figure now declares.
|
||||
jouleclaw-energy = { path = "../jouleclaw-energy", default-features = false }
|
||||
jouleclaw-cascade = { path = "../jouleclaw-cascade" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Token-Jaccard nearest-episode router.
|
||||
|
||||
use jouleclaw_energy::Provenance;
|
||||
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
||||
use jouleclaw_cascade::types::{Query, QueryInput, TierId};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -136,6 +137,7 @@ impl Router for LearnedRouter {
|
|||
RoutingPlan {
|
||||
tier_order,
|
||||
router_joules: crate::ROUTING_JOULES,
|
||||
router_energy_class: Provenance::Estimator,
|
||||
reasoning: format!(
|
||||
"L5: {} similar episodes voted; leading tier {}",
|
||||
neighbours.len(),
|
||||
|
|
|
|||
|
|
@ -71,3 +71,73 @@ pub use privacy::{
|
|||
/// [`RoutingPlan`]: jouleclaw_cascade::router::RoutingPlan
|
||||
/// [`RoutingPlan::router_joules`]: jouleclaw_cascade::router::RoutingPlan::router_joules
|
||||
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
|
||||
//! proximity over learned embeddings instead of token overlap.
|
||||
|
||||
use jouleclaw_energy::Provenance;
|
||||
use jouleclaw_cascade::router::{Router, RoutingPlan};
|
||||
use jouleclaw_cascade::types::{Query, TierId};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -172,6 +173,7 @@ impl<E: PhasorEmbedder> Router for PhasorRouter<E> {
|
|||
RoutingPlan {
|
||||
tier_order,
|
||||
router_joules: crate::ROUTING_JOULES,
|
||||
router_energy_class: Provenance::Estimator,
|
||||
reasoning: format!("L5/phasor: {} neighbours voted", scored.len()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
//! "walk everything" fallback, [`PrivacyRouter`] still produces a
|
||||
//! filtered ordered plan from `tier_coords`.
|
||||
|
||||
use jouleclaw_energy::Provenance;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use jouleclaw_cascade::coord::Coord;
|
||||
|
|
@ -502,6 +503,9 @@ fn filter_plan(
|
|||
RoutingPlan {
|
||||
tier_order: filtered,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -640,6 +644,55 @@ mod tests {
|
|||
|
||||
// ── 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.
|
||||
struct StaticRouter(Vec<TierId>);
|
||||
impl Router for StaticRouter {
|
||||
|
|
@ -648,6 +701,7 @@ mod tests {
|
|||
tier_order: self.0.clone(),
|
||||
router_joules: 0.0,
|
||||
reasoning: "static".to_string(),
|
||||
router_energy_class: Provenance::Estimator,
|
||||
}
|
||||
}
|
||||
fn estimate_overhead(&self, _q: &Query) -> f64 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue