open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-prism/tests/drafter_tier.rs
Transaction Science 05dd2e0c4c jouleclaw Phase 4: model tiers — liquid + prism + mrl + lmm + ebm
Five tier crates ported. 589 tests pass across Phase 1-4, zero
failures, zero warnings.

jouleclaw-liquid
  Ported from pattern-lang/liquid (~1123 LOC, 5 files).
  L3 tier — Closed-form Continuous-time (CfC) recurrent language
  models. The SSM class with O(L) state evolution vs transformer
  O(L²) attention. This is the v0.3 "SSM bet" from the architecture
  proposal — already shipping in the JouleClaw reference impl on
  day one.

jouleclaw-prism
  Ported from pattern-lang/prism (~3066 LOC, 6 files).
  L3 tier — 1-bit / ternary BitNet-class kernels. Ternary transformer
  forward pass. The lowest-joule local-model tier. Provides the
  TernaryDecoder backbone that jouleclaw-lmm builds on.

jouleclaw-mrl
  Ported from pattern-lang/mrl (~1464 LOC, 7 files).
  L2 tier — Matryoshka Representation Learning nested embeddings.
  Sub-millijoule retrieval tier. Embedder + GGUF embedder +
  matryoshka truncation + nearest-neighbor picker + retrieval.

jouleclaw-lmm
  Ported from pattern-lang/lmm (~1819 LOC, 6 files).
  L3 tier — Large Multimodal Model. Vision + text + audio pipeline
  on top of jouleclaw-prism's TernaryDecoder backbone. Real VL path
  (LFM2.5-VL preprocessing via the `image` crate, JPEG/PNG only —
  keeps the dep footprint minimal). The internal `prism::` references
  rewritten to `jouleclaw_prism::` during port.

jouleclaw-ebm
  Ported from pattern-lang/ebm (~1738 LOC, 7 files).
  Energy-based constraint-satisfaction tier — SAT, n-queens, sudoku,
  graph coloring, generic solver. Deterministic compute priced in
  joules, sits at the L1/L3 boundary depending on the problem class.

Bare crate-name references in integration tests fixed:
  `use liquid::...` → `use jouleclaw_liquid::...` (and same for the
  other 4 ported tiers). Initial sed pass only caught `joule_*`
  patterns; this second pass caught the bare-name ones.

gemma4 deferred. It depends on `joule-l1` and `pattern-core`
(OpenIE IP that doesn't port). Will land in Phase 6 once the
thin `LawfulTier` trait surface ships in jouleclaw-cascade or a
new jouleclaw-l1 crate so consumers can plug their own lawful
lexicon in.

Phase 5 next: omni-modal generation — diffusion (40+ samplers from
efficient-genai's ALGORITHM_CATALOG), Stable Diffusion / SDXL / SD3 /
Flux paths, MusicGen, Whisper, Gaussian3D, video synthesis,
cross-modal fusion. Source codebase: efficient-genai.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:39:05 -04:00

101 lines
3.5 KiB
Rust

//! Production-surface test: `PrismTier::with_drafter(...)` glues
//! Bonsai-1.7B as drafter onto Bonsai-4B target, dispatched through
//! the cascade as a single tier.
//!
//! Validates:
//! - The builder loads both models and rejects mismatched vocab
//! sizes at build time.
//! - `try_answer` routes through `jouleclaw_runtime::extend_with_drafter`
//! when a drafter is configured, producing a correct answer.
//! - The drafter path produces semantically-correct output (contains
//! "Paris" for the canonical capital query).
//!
//! `#[ignore]` — needs Bonsai 1.7B and 4B GGUFs on disk.
//!
//! Run:
//! cargo test --release -p prism --test drafter_tier
//! -- --ignored --nocapture
use jouleclaw_cascade::{
AnswerOutput, ContextRef, JouleBudget, QualityFloor, Query, QueryInput, Tier,
};
use jouleclaw_prism::PrismTier;
use std::time::Instant;
fn bonsai_path(n: &str) -> String {
format!("../../models/{n}")
}
fn text_query(s: &str) -> Query {
Query {
input: QueryInput::Text(s.to_string()),
// Drafter path runs BOTH models — total joules scales up.
// expensive() is the right ceiling.
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
#[test]
#[ignore]
fn prism_tier_with_drafter_answers_correctly() {
let target_path = bonsai_path("ternary-bonsai-4b-q2_0.gguf");
let drafter_path = bonsai_path("ternary-bonsai-1.7b-q2_0.gguf");
eprintln!("target: {target_path}");
eprintln!("drafter: {drafter_path}");
let t0 = Instant::now();
let mut tier = PrismTier::from_gguf(42, &target_path, 128)
.expect("target load")
.with_max_new_tokens(16)
.with_drafter(&drafter_path, 128)
.expect("drafter load")
.with_drafter_lookahead(4);
eprintln!("loaded both models in {:?}", t0.elapsed());
let q = text_query("The capital of France is");
let t = Instant::now();
let ans = tier.try_answer(&q, q.budget.hard_limit).expect("answer");
let wall = t.elapsed();
let text = match &ans.output {
AnswerOutput::Text(s) => s.clone(),
other => panic!("expected Text, got {:?}", other),
};
eprintln!(
"VERDICT: wall={:.3}s joules_spent={:.1} mJ text={:?}",
wall.as_secs_f64(), ans.joules_spent * 1e3, text,
);
assert!(text.to_lowercase().contains("paris"),
"PrismTier with drafter must reach 'Paris': {text:?}");
assert!(ans.joules_spent > 1e-3,
"drafter path should spend > 1 mJ (target+drafter compute); got {} J",
ans.joules_spent);
}
#[test]
#[ignore]
fn with_drafter_rejects_mismatched_vocab() {
// Bonsai-1.7B (qwen3, vocab ~152K) and lfm2-350M (different vocab
// family) must not pair. The build-time vocab-size check catches
// it before any inference attempt.
let target_path = bonsai_path("ternary-bonsai-1.7b-q2_0.gguf");
let mismatch_path = bonsai_path("lfm2-350m-q8_0.gguf");
// Both files must exist for this test to be meaningful — skip if
// either is missing.
if !std::path::Path::new(&mismatch_path).exists() {
eprintln!("skipping: {mismatch_path} not present");
return;
}
let tier = PrismTier::from_gguf(1, &target_path, 128)
.expect("target load")
.with_drafter(&mismatch_path, 128);
assert!(tier.is_err(),
"expected vocab-mismatch rejection when pairing Bonsai with LFM2");
eprintln!("vocab mismatch correctly rejected");
}