open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-program/src/record.rs
Transaction Science c73fe87ed9 jouleclaw: full L0–L10 cascade tier surface — 20 pure-Rust crates
Port the complete verity-cascade layer taxonomy into the public
JouleClaw open standard as 20 standalone crates, each implementing the
jouleclaw-cascade Tier (or meta-control) trait. All deterministic-first,
pure Rust, no C++/Python deps, no OpenIE IP. +541 lib tests (1141→1682),
0 failures.

Execution tiers (L0.25–L4.5):
- jouleclaw-formula        L0.25  structural-contrast relationship resolver
- jouleclaw-tool-tier      L0.5   484-tool deterministic dispatch adapter
- jouleclaw-ssm-router     L0.75  local intent classifier (keyword default)
- jouleclaw-local-index    L1     BM25 in-process retrieval
- jouleclaw-graph-rag      L1.25  entity extraction + KG enrichment
- jouleclaw-struct-contrast L1.375 graph-enriched second formula pass
- jouleclaw-ssm-reader     L1.5   extractive reading-comprehension QA
- jouleclaw-federation     L2     multi-provider parallel search + fusion
- jouleclaw-rerank         L2.5   reranker (BM25 default; ColBERT/SPLADE plug-in)
- jouleclaw-llm-cheap      L3     cheap-LLM backend trait surface
- jouleclaw-verification-tier L4  cross-model agreement verification
- jouleclaw-proof-tier     L4.5   deterministic CSP (DPLL SAT + sudoku)

Meta-cognitive control plane (L5–L10):
- jouleclaw-routing        L5     episode-memory learned dispatch ordering
- jouleclaw-agent          L6     multi-step recursive cascade
- jouleclaw-reflection     L7     offline promote/demote lessons
- jouleclaw-tuner          L8     five damped control loops
- jouleclaw-supervisor     L9     pathology detection
- jouleclaw-governor       L10    rolling joule budget + tenant quotas + kill switch

Standalone capability crates:
- jouleclaw-decode         grammar-constrained decoding (regex/CFG/JSON-schema → token mask)
- jouleclaw-program        DSPy-style typed-signature modules + compiler

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-27 09:21:25 -04:00

223 lines
7.2 KiB
Rust

//! Runtime values flowing through a compiled program.
//!
//! A [`Record`] is a `HashMap<String, Value>`. A [`Value`] is the runtime
//! complement of [`FieldType`](crate::signature::FieldType): a small enum over
//! scalar, list, and free-JSON values.
//!
//! [`Value`] is intentionally *not* `serde_json::Value` directly so that the
//! runtime can keep a typed Bool/Int/Float/Text distinction without leaning on
//! serde_json's number coercion rules. JSON-typed fields stash their payload
//! in [`Value::Json`] and let the caller schema-check.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::error::{Error, Result};
use crate::signature::FieldType;
/// Runtime value carried by a [`Record`].
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum Value {
Text(String),
Bool(bool),
Int(i64),
Float(f64),
List(Vec<Value>),
Json(JsonValue),
/// String value bound by a [`FieldType::OneOf`] enum.
Enum(String),
}
impl Value {
/// Short human label for use in error messages.
pub fn label(&self) -> &'static str {
match self {
Value::Text(_) => "text",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::List(_) => "list",
Value::Json(_) => "json",
Value::Enum(_) => "enum",
}
}
/// Convenience text constructor.
pub fn text(s: impl Into<String>) -> Self {
Value::Text(s.into())
}
/// Convenience enum constructor.
pub fn enum_variant(s: impl Into<String>) -> Self {
Value::Enum(s.into())
}
/// Lower a value to JSON for serialization-friendly logging.
pub fn to_json(&self) -> JsonValue {
match self {
Value::Text(s) => JsonValue::String(s.clone()),
Value::Bool(b) => JsonValue::Bool(*b),
Value::Int(i) => JsonValue::Number((*i).into()),
Value::Float(f) => serde_json::Number::from_f64(*f)
.map(JsonValue::Number)
.unwrap_or(JsonValue::Null),
Value::List(items) => JsonValue::Array(items.iter().map(Value::to_json).collect()),
Value::Json(v) => v.clone(),
Value::Enum(s) => JsonValue::String(s.clone()),
}
}
/// Check this value against a declared [`FieldType`].
pub fn matches(&self, ty: &FieldType) -> bool {
match (self, ty) {
(Value::Text(_), FieldType::Text) => true,
(Value::Bool(_), FieldType::Bool) => true,
(Value::Int(_), FieldType::Int) => true,
(Value::Float(_), FieldType::Float) => true,
(Value::Int(_), FieldType::Float) => true, // ints widen
(Value::List(items), FieldType::List(inner)) => {
items.iter().all(|v| v.matches(inner))
}
(Value::Json(_), FieldType::Json(_)) => true,
(Value::Enum(s), FieldType::OneOf(variants)) => variants.iter().any(|v| v == s),
(Value::Text(s), FieldType::OneOf(variants)) => variants.iter().any(|v| v == s),
_ => false,
}
}
/// Type-check a value against a field type, returning a typed error if
/// the value doesn't match.
pub fn check(&self, ty: &FieldType, module: &str, field: &str) -> Result<()> {
if self.matches(ty) {
Ok(())
} else {
Err(Error::TypeMismatch {
module: module.to_string(),
field: field.to_string(),
expected: ty.label(),
actual: self.label().to_string(),
})
}
}
}
/// Bag of named values flowing through a compiled program.
///
/// Records are passed *by value* between modules — the runner accumulates a
/// single global record by merging each module's outputs back in.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct Record {
pub values: HashMap<String, Value>,
}
impl Record {
/// Empty record.
pub fn new() -> Self {
Self::default()
}
/// Insert a value, returning the previous one if any.
pub fn insert(&mut self, key: impl Into<String>, value: Value) -> Option<Value> {
self.values.insert(key.into(), value)
}
/// Look up a value by key.
pub fn get(&self, key: &str) -> Option<&Value> {
self.values.get(key)
}
/// Number of bindings.
pub fn len(&self) -> usize {
self.values.len()
}
/// Whether this record holds no values.
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
/// Merge `other` into self, overwriting on conflict. Used by the runner
/// to fold a module's outputs back into the global program record.
pub fn merge(&mut self, other: Record) {
for (k, v) in other.values {
self.values.insert(k, v);
}
}
/// Convenience builder.
pub fn with(mut self, key: impl Into<String>, value: Value) -> Self {
self.insert(key, value);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_round_trip_basic() {
let mut r = Record::new();
r.insert("a", Value::text("hi"));
r.insert("b", Value::Int(7));
assert_eq!(r.get("a"), Some(&Value::Text("hi".into())));
assert_eq!(r.get("b"), Some(&Value::Int(7)));
assert_eq!(r.len(), 2);
}
#[test]
fn merge_overwrites() {
let mut a = Record::new();
a.insert("x", Value::Int(1));
let mut b = Record::new();
b.insert("x", Value::Int(2));
a.merge(b);
assert_eq!(a.get("x"), Some(&Value::Int(2)));
}
#[test]
fn value_matches_basic_types() {
assert!(Value::Text("a".into()).matches(&FieldType::Text));
assert!(Value::Bool(true).matches(&FieldType::Bool));
assert!(Value::Int(3).matches(&FieldType::Int));
assert!(Value::Float(1.0).matches(&FieldType::Float));
assert!(Value::Int(3).matches(&FieldType::Float)); // int widens
assert!(!Value::Float(1.0).matches(&FieldType::Int));
assert!(!Value::Text("a".into()).matches(&FieldType::Int));
}
#[test]
fn value_matches_one_of() {
let ty = FieldType::OneOf(vec!["a".into(), "b".into()]);
assert!(Value::Enum("a".into()).matches(&ty));
assert!(Value::Text("a".into()).matches(&ty));
assert!(!Value::Enum("z".into()).matches(&ty));
}
#[test]
fn value_matches_list_recursively() {
let ty = FieldType::List(Box::new(FieldType::Int));
let ok = Value::List(vec![Value::Int(1), Value::Int(2)]);
let bad = Value::List(vec![Value::Int(1), Value::Text("x".into())]);
assert!(ok.matches(&ty));
assert!(!bad.matches(&ty));
}
#[test]
fn value_check_returns_type_mismatch() {
let err = Value::Text("x".into())
.check(&FieldType::Int, "m", "f")
.unwrap_err();
assert!(matches!(err, Error::TypeMismatch { .. }));
}
#[test]
fn value_to_json_round_trips_through_serde() {
let v = Value::List(vec![Value::Int(1), Value::Bool(true)]);
let j = v.to_json();
assert_eq!(j, serde_json::json!([1, true]));
}
}