Cast turns a short English prompt into a query plan, using a 3.33M-parameter model that runs locally on CPU — no API key, no network call, no per-token bill. Ten clauses and six operators is the whole grammar; small enough that a small model can learn it completely, and small enough that shipping every query to a frontier model is an absurd amount of machinery.
The safety properties
| Property | What it means |
|---|---|
| The model never executes | It emits text; the text goes through the same query path a hand-typed query uses. There is no second executor to audit. |
| Validation is parsing | Parse and execute share one code path, so they cannot disagree about what is well-formed. Invalid output is refused with the offending text. |
execute defaults to false | You get a plan for review. Running a guess silently is worse than admitting uncertainty. |
That last default earns its keep. A real miss: "paid orders over 100" planned as
LIMIT 100 instead of total > 100 — and the count came back
correct anyway, because both paid orders happened to exceed 100. A count-only assertion
would have scored it a pass. A human reading the plan catches it; an auto-executing client
does not.
Why the planner lives in the engine
The hard part of NL querying is the schema, and a client's copy is stale on arrival. The engine holds the live collection list, so a plan naming a collection that does not exist returns 422 with the reason — never silently empty rows, because zero rows reads as "no matching data", which would be a lie. Every daemon client inherits this.
Drift — the failure valid cannot catch
A model outside its vocabulary substitutes a memorised literal:
"memories about pricing" became SEARCH "handoff" — which parses, names a real
collection, returns real rows, and answers a question nobody asked. Measured on the released
checkpoint: in-vocabulary terms copied 3/3; out-of-vocabulary terms 0/3. So the response carries
a drift field when a quoted literal does not appear in the prompt — advisory, never
fatal, validated at 24/24 with zero false alarms (correctly inferred enum values stay silent).
An unattended caller should gate on all three:
plan["valid"] and plan["collection_known"] and not plan.get("drift").
Where it is strong (and weak) — measured
| Clause | Exact-plan match (eval) |
|---|---|
TRACE caused_by | 96.5% |
TRAVERSE | 93.3% |
single WHERE | 91.2% |
LIMIT | 91.1% |
SEARCH | 90.5% |
ORDER BY | 87.7% |
two+ WHERE | 85.1% (61.2% adversarial holdout) |
GROUP BY + aggregate | 77.0% |
Two habits avoid most misses: name the field when a number could be a limit ("orders
with total over 100" beats "orders over 100"), and check numbers over four digits —
digits tokenize one at a time, so 400000 can come back 4000. The model
card publishes every failure mode with examples.
Enabling it
# compile-time: cargo install nedb-engine --features cast
# weights (~13 MB): GitHub release asset, checksum-verified on load
nedbd --dag --cast ./data
Off by default, feature-gated at compile time and flag-gated at runtime. Built without the feature the route returns 501 (not 404) so clients can detect the capability. Without weights, the daemon logs loudly and serves everything else normally.
caused_by. The engine did not just host the model; it built and audited
it.tools/build_docs_site.py.