Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ harness = false
name = "bench_pipeline"
harness = false

[[example]]
name = "cost_cascade"

[lib]
crate-type = ["rlib"]
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,50 @@ Every inferred name gets a confidence score:

---

## 💸 Cost Cascade (confidence-gated model routing)

The confidence score above isn't just a label — it's a **routing signal**. The
`cascade` module wires a cost cascade onto it: run a **cheap** model first, and
**escalate to an expensive (frontier) model only when confidence < threshold**.
Most names are recovered cheaply; you only pay the frontier price for the hard,
low-confidence ones. Pure cost-Pareto — no accuracy is lost, because the cheap
answer is kept whenever it's already confident enough.

```rust
use ruvector_decompiler::cascade::{CascadeInferrer, CorpusTier, NameInferrer};

// Cheapest-first tiers. The $0 corpus tier handles the easy cases;
// `MyFrontierTier` (your model — local transformer or remote API) only runs
// when the corpus answer is below the threshold.
let tiers: Vec<Box<dyn NameInferrer>> = vec![
Box::new(CorpusTier::builtin()), // cost 0.0 ($0)
Box::new(MyFrontierTier::new()), // cost 100 (pay only on escalation)
];
let mut cascade = CascadeInferrer::new(tiers, CascadeInferrer::DEFAULT_THRESHOLD); // 0.9
let names = cascade.infer_modules(&modules);

let stats = cascade.stats();
println!("cheap wins: {:.0}% cost saved: {:.0}% vs frontier-only",
stats.cheap_win_rate() * 100.0,
stats.cost_saved() / stats.frontier_only_cost * 100.0);
```

**Default = unchanged.** The standard `decompile()` pipeline does *not* use the
cascade — it's strictly opt-in (a single-tier cascade is identical to the
existing inferrer). Bring your own frontier tier by implementing the
`NameInferrer` trait (one method); no provider is hardcoded.

**Self-tuning.** Every inference records a `CascadeOutcome` (which tier answered,
its confidence, whether escalation changed the answer). `cascade.self_tune(step)`
reads that log and adjusts the threshold for the next run — lowering it when the
frontier keeps *confirming* the cheap tier (stop paying for confirmations),
raising it when the frontier keeps *overturning* it. This closes the loop with
ruDevolution's "gets smarter every run" self-learning.

Runnable, model-free demo (deterministic, $0): `cargo run --example cost_cascade`.

---

<details>
<summary><strong>📖 Tutorial: Decompile an npm Package</strong></summary>

Expand Down
118 changes: 118 additions & 0 deletions examples/cost_cascade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Confidence-gated cost cascade for AI name recovery (no real model calls).
//!
//! Demonstrates the metaharness cost-cascade thesis mapped onto rudevolution's
//! existing per-inference confidence score: run a cheap tier first, escalate to
//! a frontier tier only when confidence < threshold.
//!
//! This example uses the built-in $0 corpus tier as the cheap tier and a
//! *deterministic stand-in* for the frontier tier — it makes NO network calls
//! and loads NO model. Swap `MockFrontierTier` for a real backend
//! (`neural::NeuralInferrer`, an HTTP client to `@metaharness/router`, etc.) by
//! implementing the same `NameInferrer` trait.
//!
//! Usage: cargo run --example cost_cascade

use ruvector_decompiler::cascade::{CascadeInferrer, CorpusTier, NameInferrer};
use ruvector_decompiler::inferrer::InferenceContext;
use ruvector_decompiler::types::{DeclKind, Declaration, InferredName, Module};

/// Deterministic stand-in for a frontier model. In production this would call a
/// real model (local transformer or remote API). Here it just returns a
/// plausible high-confidence name so the example is reproducible and $0.
struct MockFrontierTier;

impl NameInferrer for MockFrontierTier {
fn label(&self) -> &str {
"frontier(mock)"
}
fn cost(&self) -> f64 {
100.0 // relative cost units vs corpus = 0.0
}
fn infer(&self, decl: &Declaration, ctx: &InferenceContext) -> Option<InferredName> {
// A real frontier model would reason over `ctx`. The mock derives a
// deterministic name from the available signal so output is stable.
let hint = ctx
.property_accesses
.first()
.or_else(|| ctx.string_literals.first())
.cloned()
.unwrap_or_else(|| decl.kind.to_string());
Some(InferredName {
original: decl.name.clone(),
inferred: format!("frontier_{hint}"),
confidence: 0.93,
evidence: vec!["mock frontier model (deterministic, $0)".to_string()],
})
}
}

fn decl(name: &str, strings: &[&str], props: &[&str]) -> Declaration {
Declaration {
name: name.to_string(),
kind: DeclKind::Var,
byte_range: (0, 4),
string_literals: strings.iter().map(|s| s.to_string()).collect(),
property_accesses: props.iter().map(|s| s.to_string()).collect(),
references: vec![],
}
}

fn main() {
// A mix: some declarations the cheap corpus tier recovers confidently
// (high-confidence known patterns), some it can only guess at (low conf).
let module = Module {
name: "bundle".to_string(),
index: 0,
declarations: vec![
decl("a", &["tools/call"], &[]), // corpus: high conf -> cheap win
decl("b", &["authenticate"], &[]), // corpus: high conf -> cheap win
decl("c", &[], &[]), // corpus: weak/none -> escalate
decl("d", &[], &["weirdProp"]), // corpus: weak -> escalate
],
source: String::new(),
byte_range: (0, 0),
};

// Cheapest-first tiers. Threshold 0.9 matches the crate's "High" confidence.
let tiers: Vec<Box<dyn NameInferrer>> = vec![
Box::new(CorpusTier::builtin()),
Box::new(MockFrontierTier),
];
let mut cascade = CascadeInferrer::new(tiers, CascadeInferrer::DEFAULT_THRESHOLD);

let names = cascade.infer_modules(&[module]);

println!("Recovered {} names (threshold {:.2}):\n", names.len(), cascade.threshold());
for o in cascade.outcomes() {
println!(
" {:<4} -> tier={:<14} conf={:.2} escalated={} cost={}",
o.original, o.winning_tier, o.confidence, o.escalated, o.cost
);
}

let stats = cascade.stats();
println!("\nCascade stats:");
println!(" total : {}", stats.total);
println!(
" cheap wins : {} ({:.0}%)",
stats.cheap_wins,
stats.cheap_win_rate() * 100.0
);
println!(" escalations : {}", stats.escalations);
println!(" total cost : {}", stats.total_cost);
println!(" frontier-only : {}", stats.frontier_only_cost);
println!(
" cost saved : {} ({:.0}% vs frontier-only)",
stats.cost_saved(),
if stats.frontier_only_cost > 0.0 {
stats.cost_saved() / stats.frontier_only_cost * 100.0
} else {
0.0
}
);

// Self-tuning: feed the recorded outcomes back into the threshold for the
// next run. This closes the loop with rudevolution's self-learning design.
let next = cascade.suggest_threshold(0.05);
println!("\nSelf-tuned threshold for next run: {next:.3}");
}
Loading