budgetkernel 0.1.2

A small, auditable, deterministic budget accounting kernel with zero heap allocation on the hot path.
Documentation
# budgetkernel


[![CI](https://github.com/Qarait/budgetkernel/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/Qarait/budgetkernel/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/budgetkernel.svg)](https://crates.io/crates/budgetkernel)
[![Docs.rs](https://docs.rs/budgetkernel/badge.svg)](https://docs.rs/budgetkernel)

A small, auditable, deterministic budget accounting kernel with zero heap allocation on the hot path.

Declare budgets across fixed dimensions, charge them at runtime boundaries, and get a verdict:

```text
Continue
Warn(dim)
Exhausted(dim)
```

The crate is intentionally narrow. It does not read clocks, perform I/O, perform heap allocation on the hot path, refill budgets automatically, persist state, or coordinate across machines. The caller owns measurement and policy. `budgetkernel` owns bounded accounting.

## Why this exists


LLM pipelines, task runners, crawlers, quota systems, and agent loops often need to track more than one resource at once:

1. tokens
2. elapsed milliseconds
3. bytes
4. calls
5. memory
6. caller-defined custom units

Most systems do this with ad-hoc counters. `budgetkernel` provides a small kernel for that accounting with explicit semantics and a well-defined verification story.

It is not a rate limiter. It is not a metrics system. It is not a distributed quota service.

It is the deterministic core those systems can build around.

## Example


```rust
use budgetkernel::{Budget, Dim, Verdict};

fn main() -> Result<(), Box<dyn std::error::Error>> {
	let mut budget = Budget::builder()
		.limit_with_warn(Dim::Tokens, 10_000, 8_000)
		.limit_with_warn(Dim::Millis, 30_000, 27_000)
		.limit_with_warn(Dim::Calls, 5, 4)
		.build()?;

	let mut verdict = Verdict::Continue;

	verdict = verdict.worst(budget.charge(Dim::Tokens, 1_523)?);
	verdict = verdict.worst(budget.charge(Dim::Millis, 842)?);
	verdict = verdict.worst(budget.charge(Dim::Calls, 1)?);

	match verdict {
		Verdict::Continue => {
			// Keep going.
		}
		Verdict::Warn(dim) => {
			// Still allowed, but consider degrading or preempting.
			println!("warning: {} budget is getting low", dim.name());
		}
		Verdict::Exhausted(dim) => {
			// Stop and return a partial result.
			println!("exhausted: {}", dim.name());
		}
	}

	Ok(())
}
```

For complete examples, see:

1. [examples/llm_pipeline.rs]https://github.com/Qarait/budgetkernel/blob/master/examples/llm_pipeline.rs
2. [examples/task_runner.rs]https://github.com/Qarait/budgetkernel/blob/master/examples/task_runner.rs
3. [examples/http_quota.rs]https://github.com/Qarait/budgetkernel/blob/master/examples/http_quota.rs

## Core API


Build a budget:

```rust
let mut budget = Budget::builder()
	.limit(Dim::Calls, 50)
	.limit_with_warn(Dim::Tokens, 100_000, 80_000)
	.build()?;
```

Charge one dimension:

```rust
let verdict = budget.charge(Dim::Tokens, 1_000)?;
```

Query accounting state:

```rust
let spent = budget.spent(Dim::Tokens);
let remaining = budget.remaining(Dim::Tokens);
```

Manually reset spent counters:

```rust
budget.reset();
```

`reset()` preserves declared limits and warn thresholds. It does not perform automatic refill. The caller decides when a budget period ends.

## Dimensions


The dimension set is fixed:

```rust
pub enum Dim {
	Tokens,
	Millis,
	Bytes,
	Calls,
	Memory,
	Custom0,
	Custom1,
	Custom2,
}
```

There are exactly eight dimensions.

The fixed set is deliberate. It avoids dynamic registration, string keys, hashing, allocation, and user-provided discriminants. Internally, dimensions map to dense array indexes.

The three `Custom` slots are for caller-defined units. For example, an adapter may define `Custom0` as "work units" or "retrieval depth" in its own codebase.

## Verdicts


`Budget::charge` returns:

```rust
Result<Verdict, ChargeError>
```

`ChargeError` reports structural errors, such as charging an undeclared dimension.

`Verdict` reports the state of an accepted charge:

```rust
pub enum Verdict {
	Continue,
	Warn(Dim),
	Exhausted(Dim),
}
```

### Continue


The charge was accepted and no configured warn threshold was crossed.

### Warn


The charge was accepted, but the running total is now above the configured warn threshold.

Warn is not one-shot. It fires on every charge where the current state is above the warn threshold but not exhausted. If callers want one-shot logging, they should track suppression in their adapter.

### Exhausted


The charge pushed the running total past the configured limit.

Limits are inclusive:

```text
spent == limit     => Continue or Warn
spent > limit      => Exhausted
```

Exhaustion wins over warning. If one charge crosses both the warn threshold and the limit, the verdict is `Exhausted`.

## Sequential multi-dimension checkpoints


v0.1 intentionally ships single-dimension charging only:

```rust
budget.charge(dim, amount)?;
```

Callers who want to check several dimensions at one checkpoint can perform several sequential charges and reduce the verdicts with `Verdict::worst`:

```rust
let mut verdict = Verdict::Continue;

verdict = verdict.worst(budget.charge(Dim::Tokens, tokens)?);
verdict = verdict.worst(budget.charge(Dim::Millis, millis)?);
verdict = verdict.worst(budget.charge(Dim::Calls, 1)?);
```

This is not atomic batch charging. It is a deterministic reduction of sequential results.

Batch charging may be added later if real usage justifies it.

## Zero charges and `warn = 0`


Charging zero is valid:

```rust
budget.charge(dim, 0)?;
```

A zero charge reports the verdict for the current state without increasing the reported spent value. This can be used as a state poll.

A warn threshold of zero is also valid. It means the first positive spend enters the warning state.

## Design guarantees


The crate is designed around these guarantees:

1. no heap allocation on the hot path
2. no clocks
3. no I/O
4. no syscalls
5. deterministic behavior
6. saturating arithmetic
7. bounded work
8. no caller-triggerable panics
9. `no_std` compatibility
10. one current unsafe boundary, isolated in the internal fixed map implementation

The no-panic guarantee means no caller-triggerable panics from valid API usage. Internal `debug_assert!` checks may guard kernel invariants during debug builds.

See [docs/DESIGN.md](https://github.com/Qarait/budgetkernel/blob/master/docs/DESIGN.md) for the full design rationale.

## Safety model


The default internal map uses `MaybeUninit<u64>` plus presence bits. This is the only current unsafe boundary.

The `safe-map` feature replaces that implementation with fully initialized arrays and removes unsafe from the map implementation.

Both variants are tested with the same unit and property tests.

See [docs/SECURITY_MODEL.md](https://github.com/Qarait/budgetkernel/blob/master/docs/SECURITY_MODEL.md) for invariants, threat model, lint posture, and verification details.

## Feature flags


```toml
[features]
default = ["std"]
std = []
safe-map = []
```

### `std`


Enabled by default.

Adds `std::error::Error` implementations for error types.

The core accounting logic does not require `std`.

### `safe-map`


Uses a fully safe internal fixed map implementation.

This is useful for audits or policy environments that prefer no unsafe code in the internal map. The public behavior is identical.

### `no_std`


Build without default features:

```bash
cargo build --no-default-features
```

The crate remains usable without `std`. The caller is still responsible for measurement, clocks, logging, persistence, and any adapter behavior.

## Verification


Current verification matrix:

```text
cargo build
cargo build --no-default-features
cargo build --features safe-map
cargo build --no-default-features --features safe-map

cargo test
cargo test --no-default-features
cargo test --features safe-map
cargo test --no-default-features --features safe-map

cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
cargo doc --no-deps
cargo doc --no-deps --no-default-features
cargo +nightly miri test --lib
```

The test suite includes:

1. deterministic unit tests
2. public API property tests
3. doctests
4. tests under `safe-map`
5. tests under `no_std`
6. MIRI for library tests

## Benchmarks


A Criterion benchmark suite is included:

```bash
cargo bench --bench charge
cargo bench --bench charge --features safe-map
```

It measures:

1. continuing single-dimension charge
2. warning single-dimension charge
3. already-exhausted single-dimension charge
4. three sequential charges as a checkpoint pattern

Benchmark numbers are local measurements and vary by CPU, compiler, target, optimization level, and feature configuration. Treat them as a baseline for your environment, not a universal guarantee.

## Non-goals


`budgetkernel` does not provide:

1. automatic time-based refill
2. rate limiting
3. background tasks
4. persistence
5. distributed coordination
6. async APIs
7. dynamic dimension registration
8. model pricing tables
9. clocks
10. I/O
11. logging

Adapters can build those behaviors around the kernel.

## Status


Released on crates.io.

Current version: `0.1.1`.

The core kernel, examples, property tests, benchmarks, design/security docs, README, CHANGELOG, CI, and release packaging are in place.