The database that knows what it doesn't know.
nescio stores claims — who said what, when, and how reliable they are. Credible regions, entropy and answers are derived at query time, and they decay the way the real world does.
startInstall
nescio is a single binary with an embedded engine — no daemon, no dependencies. A database is just a directory.
cargo install nescio
Or use it as a Rust library:
cargo add nescio
Requires Rust 1.75+. Sources are on GitHub; the crate API reference lives on docs.rs.
startFive-minute tour
Every output below is the actual output of nescio v0.8.0 — copy the commands and you will get the same numbers.
1 · Create a database
The real-estate template ships four slots and five sources of very
different trustworthiness. (nescio templates lists all built-ins —
osint and sensor too — and nescio templates --show
NAME prints one as JSON to start your own schema from.)
$ nescio init mydb --template real-estate
initialized nescioDB in mydb
slots: condition, price, wants_to_sell, year_built
sources: broker, land_registry, neighbor, notary, web_scraper
2 · Ingest a claim
You never write a value. You record that someone said something:
$ nescio ingest mydb --entity villa_1 --slot price \
--interval 900000..1000000 --source broker --at 2026-06-25
ingested evidence for villa_1.price at 2026-06-25
3 · Ask what is known
$ nescio bound mydb --entity villa_1 --slot price --at 2026-07-03
BOUND villa_1.price as of 2026-07-03
region (95%): [570000, 1210000]
entropy: 4.20 of 7.64 bits (knowledge 45%)
MAP estimate: 905000
The broker said 900–1,000k eight days ago with reliability 0.85 and a 90-day half-life — so the region is already wider than the claim. The database is honest about that.
4 · Time-travel
Ask the same question a year later. Nothing was written, nothing expired — the evidence just aged:
$ nescio bound mydb --entity villa_1 --slot price --at 2027-07-03
BOUND villa_1.price as of 2027-07-03
region (95%): [220000, 1570000]
entropy: 7.13 of 7.64 bits (knowledge 7%)
MAP estimate: 905000
5 · Let the database plan its own procurement
Describe what evidence could be bought, and resolve picks the
cheapest path to a knowledge target:
$ nescio resolve mydb --entity villa_1 --slot price \
--target-bits 2.0 --actions actions.json --at 2026-07-03
RESOLVE villa_1.price: 4.42 bits now, target 2.00
1. call the broker (slot price, cost 5) -> expected 2.57 bits
2. pull the land registry (slot price, cost 40) -> expected 1.50 bits
total cost 45 | greedy estimate 1.50 bits | MC-validated 1.31 bits
From here: read the concepts to understand the model, or jump straight to the verbs.
conceptsClaims, not values
A row in a classical database asserts a fact. A record in nescio asserts that a source made a claim:
{"entity": "villa_1",
"claim": {"type": "interval", "slot": "price", "lo": 900000.0, "hi": 1000000.0},
"source": "broker",
"observed_at": 1782345600}
Three claim types cover continuous and categorical slots:
| type | says | slot kind |
|---|---|---|
interval | "the value lies in [lo, hi]" | continuous |
value | "the value is v" | categorical |
not_value | "the value is not v" | categorical |
Each claim contributes a likelihood factor under a mixture model: with probability
r the source is right (the value is uniform over what it asserted), with
1 − r the claim is noise (uniform over the whole domain). Two
consequences fall out of this, for free:
- Precision carries weight. A narrow interval is stronger evidence than a vague one, from the same source at the same time.
- Contradiction is representable. Two sources can disagree; the posterior holds both, weighted by reliability.
Everything else — regions, entropy, answers — is derived at query time. There is no materialized value to become stale.
conceptsSources & decay
A source has a base reliability and, optionally, a half-life. Reliability erodes by physics, not by TTL:
r(t) = r0 · 0.5 ^ (age_days / half_life_days)
As r → 0 the claim's likelihood factor flattens toward
uniform: old evidence loosens its grip on the region automatically. Deleting is
unnecessary — and when you do delete (forgetting), regions
widen correctly because nothing was ever aggregated.
$ nescio source mydb --name web_scraper --reliability 0.7 --half-life-days 45
The template's sources show the intended spread:
| source | r0 | half-life | |
|---|---|---|---|
land_registry | 1.0 | — | axiomatic: never decays, never wrong |
notary | 1.0 | — | axiomatic |
broker | 0.85 | 90 d | |
web_scraper | 0.7 | 45 d | |
neighbor | 0.4 | 30 d | gossip, weakly informative |
Non-axiomatic reliability is capped just below 1.0 — two contradicting hard claims would otherwise annihilate the posterior. Only axioms may be absolute.
If the log contains ground truth (axiomatic evidence about the same slots a soft source reported on), nescio can learn a source's physics instead of you guessing them:
$ nescio recalibrate mydb --source web_scraper --apply
conceptsRegions & entropy
A slot's domain is discretized into cells (bins for continuous slots, one cell per
value for categorical ones). The posterior over those cells yields everything
bound reports:
- Credible region — the smallest set of cells holding (by default)
95% of the probability mass, merged into intervals. A field without evidence is not
NULL; it is a region covering the whole domain. - Entropy in bits — how ignorant the database really is.
7.64 bitsis maximal for a 200-bin slot (log₂ 200);0is a collapsed point. The ratio is reported as knowledge. - MAP estimate — the single most probable cell, if you must have one number. It is deliberately the last line of the output.
conceptsCouplings
Couplings let knowledge flow between slots of the same entity. The template couples
condition ~ price: renovated villas center on 1.3M, derelict
ones on 500k. Watch what a claim about the condition does to the
price:
$ nescio ingest mydb --entity villa_1 --slot condition \
--value renovated --source web_scraper --at 2026-06-20
$ nescio bound mydb --entity villa_1 --slot price --at 2026-07-03
BOUND villa_1.price as of 2026-07-03
region (95%): [900000, 1550000] ← was [570000, 1210000]
entropy: 4.42 of 7.64 bits (knowledge 42%)
MAP estimate: 995000 ← was 905000
The region shifted upward — and note that knowledge dropped from 45% to 42%. The scraper's "renovated" pulls toward 1.3M while the broker's interval says 900–1,000k; the posterior honestly holds the tension between them. Evidence that disagrees with what you believed makes you less certain. That is a feature.
There are four coupling kinds — gaussian_by_category,
step_threshold, matrix and table — each
expecting specific domain kinds on slot_a / slot_b. They are
declared once in schema.json and apply to every entity; the exact JSON
for each kind is in the file-format reference.
conceptsAxioms & conflict
An axiomatic source has reliability 1.0 and no half-life: the land registry, a notary, a court ruling. Axioms are the only way to make something certain — and their regions never widen:
$ nescio bound mydb --entity villa_3 --slot price --at 2036-07-03
BOUND villa_3.price as of 2036-07-03
region (95%): [940000, 990000] ← identical ten years later
entropy: 2.32 of 7.64 bits (knowledge 70%)
Two axioms that contradict each other are not a bug to paper over — they are a real
state of the world, and nescio surfaces them as a conflict when you query the affected
slot. Over HTTP that is a 409:
$ curl 'localhost:7777/bound?entity=villa_3&slot=price'
{"error":"axiom conflict: villa_3.price: axiomatic evidence conflicts"}
HTTP 409
conceptsPriors
When many entities share background knowledge ("prices in this district cluster around 800k"), register the prior once and reference it per entity — it is stored once, never copied:
$ nescio prior register mydb --name district_9 --slot price --gaussian 800000,150000
$ nescio prior use mydb --entity villa_2 --slot price --name district_9
Arbitrary shapes go in via --weights-file (one weight per cell of the
slot's domain).
conceptsSchema evolution
Nobody knows their schema on day one. A live database can grow and shrink — and the model makes most of it cheap, because ignorance is the default state:
$ nescio schema add-slot mydb --name floor_area --continuous 20..600 --bins 116
slot "floor_area" added (continuous [20, 600], 116 bins); every entity starts at maximal entropy
$ nescio schema add-value mydb --slot condition --value gutted
value "gutted" added to slot "condition" (4 values now)
$ nescio schema add-coupling mydb --file coupling.json
coupling sell~price added; it applies to every entity immediately
- Adding a slot needs no backfill — every existing entity simply
starts at maximal entropy on it. There is no
NULLto migrate. - Extending a categorical slot keeps history valid — the log stores values as strings, not indices. Coupling tables are recompiled (a category without an entry is uninformative), and priors on the slot are extended with the mean of their existing weights.
- Changes are transactional — a coupling that fails validation (unknown slot, wrong domain kind, misshapen table) is refused with nothing committed. Like a contradicting axiom, a valid coupling that disagrees with existing evidence surfaces at query time, not at schema time.
- Removing a slot cascades what is slot-local — its evidence is
physically erased (log rewrite, same mechanism as forgetting)
and its priors go with it. Anything cross-slot is refused instead:
remove-sloton a coupled slot tells you to remove the coupling first.
The same operations over HTTP: POST /schema/add-slot,
/schema/add-value, /schema/add-coupling,
/schema/remove-coupling, /schema/remove-slot.
conceptsWatches & knowledge horizons
Decay is deterministic physics — which means "when will the database stop knowing enough?" has a computable answer. A watch is a standing question: name an entity's slot and a threshold, and nescio answers with the knowledge horizon — the exact date on which decay alone will push the slot past the threshold — the moment you register it:
$ nescio watch add mydb --name price_fresh --entity villa_1 --slot price --max-entropy 5.0
watch "price_fresh" added
ok price_fresh villa_1.price 4.25 bits (threshold 5.00) fires ~2026-08-10 without new evidence
$ nescio watch add mydb --name sell_known --entity villa_1 --slot wants_to_sell --min-knowledge 0.3
watch "sell_known" added
TRIGGERED sell_known villa_1.wants_to_sell 1.00 bits (threshold 0.70)
$ nescio watch check mydb --at 2026-09-01 ← exits 2 when anything fired: cron-ready
TRIGGERED price_fresh villa_1.price 5.38 bits (threshold 5.00)
TRIGGERED sell_known villa_1.wants_to_sell 1.00 bits (threshold 0.70)
- Two ways to say the same thing —
--max-entropy BITS(absolute) or--min-knowledge RATIO(relative to the slot's maximal entropy). Exactly one per watch. - The horizon is a prediction, not a poll — no horizon exists when an axiomatic, non-decaying source pins the slot forever; new evidence moves it.
- Conflict fires the watch — a standing question that cannot be answered (axiom conflict on the slot) is treated as triggered, with the error attached. Contradiction must surface.
- Cron-friendly —
nescio watch checkexits 0 when all is quiet, 2 when something fired;--jsonfor machines.
Under nescio serve, a background evaluator re-checks the watches after
every write and pushes transitions as Server-Sent Events — decay
becomes a signal you subscribe to instead of a report you remember to ask for:
$ curl -N localhost:7777/watches/events
event: snapshot
data: {"as_of":1783281478,"watches":[{"name":"price_fresh","entity":"villa_1","slot":"price",
"max_entropy_bits":5.0,"triggered":false,"entropy_bits":4.25,"threshold_bits":5.0,
"knowledge":0.44,"horizon":1786305478,"horizon_date":"2026-08-10"}]}
event: triggered ← ~40 days later, no new evidence
data: {"name":"price_fresh","entity":"villa_1","slot":"price","triggered":true, …}
event: recovered ← fresh evidence arrived
data: {"name":"price_fresh","entity":"villa_1","slot":"price","triggered":false, …}
The matching HTTP routes: GET /watches (every watch with state and
horizon), GET /watches/check (only the triggered ones),
POST /watches / POST /watches/remove. Watches live in
watches.json next to the schema (format).
conceptsForgetting
GDPR erasure is a first-class verb. Because nothing is aggregated — every answer is derived from the raw claims — physically removing a source's evidence makes every derived region widen correctly. There is no cached aggregate that could forget to forget:
$ nescio forget-source mydb --source web_scraper
physically erased 2 evidence records from source "web_scraper"; all derived regions widen
$ nescio bound mydb --entity villa_2 --slot price --at 2026-07-03
entropy: 7.22 of 7.64 bits (knowledge 6%) ← was 6.29 bits (18%)
the verbsbound
"What is known — and how ignorant are you, really?"
$ nescio bound mydb --entity villa_1 --slot price --credible 0.95 --at 2026-07-03
BOUND villa_1.price as of 2026-07-03
region (95%): [570000, 1210000]
entropy: 4.20 of 7.64 bits (knowledge 45%)
MAP estimate: 905000
$ curl 'localhost:7777/bound?entity=villa_1&slot=price&at=2026-07-03'
{
"entity": "villa_1",
"slot": "price",
"region": [[900000.0, 1550000.0]],
"entropy_bits": 4.417295722211798,
"max_entropy_bits": 7.643856189774724,
"map_estimate": 995000.0
}
use nescio::prelude::*;
use nescio::time::now_unix;
let db = Db::open(std::path::Path::new("mydb"))?;
let q = Query::new(&db, now_unix());
let b = q.bound("villa_1", "price", 0.95)?;
println!("{:.2} bits, knowledge {:.0}%", b.entropy_bits, b.knowledge_ratio() * 100.0);
The credible level defaults to 0.95. Add --json for machine-readable
output; the region of a categorical slot is a value set instead of intervals.
the verbssample
"Show me one world that is consistent with everything you know."
$ nescio sample mydb --entity villa_1 --seed 1 --at 2026-07-03
SAMPLE villa_1 (seed 1) as of 2026-07-03
condition = renovated
price = 916328.3849294278
wants_to_sell = true
year_built = 2006.148830186706
$ curl 'localhost:7777/sample?entity=villa_1&seed=1&at=2026-07-03'
Samples respect couplings — a world where condition = derelict
drags its sampled price down. The same seed always yields the same world, which makes
downstream Monte-Carlo pipelines reproducible; different seeds explore the posterior.
the verbscertainly
"Is this true — and do you actually know it, or merely believe it?"
Three-valued logic as region containment: true if the whole region satisfies the predicate, false if none of it does, possible otherwise.
$ nescio certainly mydb --entity villa_3 --slot price --lt 1000000 --at 2026-07-03
true
$ nescio certainly mydb --entity villa_3 --slot price --gt 1000000 --at 2026-07-03
false
$ nescio certainly mydb --entity villa_1 --slot price --gt 950000 --at 2026-07-03
possible
$ curl 'localhost:7777/certainly?entity=villa_3&slot=price&op=lt&value=1000000'
{"result":"true"}
villa_3's price rests on a notarial axiom, so hard answers exist. villa_1 rests on soft evidence — under the mixture model soft evidence never zeroes out any cell, so its predicates can only ever be possible. If you need certainty, you need an axiom; the database will not fake it.
Predicates: --gt, --lt, --between LO..HI
(continuous), --is, --is-not (categorical).
the verbsfind
"Which entities certainly / possibly lie in this range?"
$ nescio find mydb --slot price --lo 900000 --hi 1000000 --mode possible --at 2026-07-03
villa_1
villa_2
villa_3
$ nescio find mydb --slot price --lo 900000 --hi 1000000 --mode certain --at 2026-07-03
villa_3
$ curl 'localhost:7777/find?slot=price&lo=900000&hi=1000000&mode=certain'
["villa_3"]
The two modes are the two useful questions: possible is recall
(candidates you must not miss), certain is precision (matches you can act
on without checking). A classical WHERE clause silently conflates them.
the verbsjoin
"Which pairs of entities relate — and how sure is that?"
Joining two regions is itself uncertain, so every match carries a probability (exact integral over the two posteriors, no sampling) and a three-valued certainty.
# comparable properties: |left.price − right.price| ≤ 150k
$ nescio join mydb --op approx --left price --right price --tol 150000 --at 2026-07-03
villa_1 villa_3 p=0.885 possible
villa_1 villa_2 p=0.164 possible
villa_2 villa_3 p=0.163 possible
3 pairs examined
# who is certainly / probably dearer than whom?
$ nescio join mydb --op gt --left price --right price --at 2026-07-03
villa_3 villa_2 p=0.753 possible
villa_1 villa_2 p=0.749 possible
...
$ curl -X POST localhost:7777/join -d '{
"predicate": {"op": "approx", "left": "price", "right": "price", "tol": 150000},
"options": {"min_probability": 0.5},
"at": "2026-07-03"
}'
{
"matches": [
{"left": "villa_1", "right": "villa_3",
"probability": 0.8849608215246407, "certainty": "possible"}
],
"pairs_examined": 3,
"truncated": false
}
Operators: gt, lt, approx (numeric,
needs --tol) and same (categorical — entity resolution and
dedup). Useful options:
| option | |
|---|---|
--min-prob 0.8 | keep only matches at least this probable |
--certain | keep only regionally-certain matches |
--left-prefix a_ | restrict a side by entity-id prefix ("kinds" by naming convention) |
--all-entities | include entities with no evidence on the slot (default: excluded — they would match everything as "possible") |
--limit 20 | cap returned matches, ranked by probability |
Pairs are pruned to the regionally possible before any integral is computed. An
unselective join is quadratic — that is fundamental to joins, SQL included — so
evaluation is capped and the cap is reported (truncated: true),
never silently applied.
the verbsresolve
"Which minimal-cost evidence would push entropy under a target?"
Describe the procurement actions you could take — each is "ask this source about this slot, at this cost":
# actions.json
[
{"name": "call the broker", "slot": "price", "cost": 5,
"source": {"name": "broker", "reliability": 0.85, "half_life_days": 90},
"answer_width": 100000},
{"name": "pull the land registry", "slot": "price", "cost": 40,
"source": {"name": "land_registry", "reliability": 1.0, "axiomatic": true},
"answer_width": 20000},
{"name": "ask the neighbor", "slot": "condition", "cost": 1,
"source": {"name": "neighbor", "reliability": 0.4, "half_life_days": 30}}
]
$ nescio resolve mydb --entity villa_1 --slot price \
--target-bits 2.0 --actions actions.json --at 2026-07-03
RESOLVE villa_1.price: 4.42 bits now, target 2.00
1. call the broker (slot price, cost 5) -> expected 2.57 bits
2. pull the land registry (slot price, cost 40) -> expected 1.50 bits
total cost 45 | greedy estimate 1.50 bits | MC-validated 1.31 bits
$ curl -X POST localhost:7777/resolve -d '{
"entity": "villa_1", "slot": "price", "target_bits": 2.0,
"actions": [{"name": "call the broker", "slot": "price", "cost": 5,
"source": {"name": "broker", "reliability": 0.85, "half_life_days": 90},
"answer_width": 100000}],
"at": "2026-07-03"
}'
Three details worth knowing:
- Actions on other slots count — couplings carry the information gain across slot boundaries. Asking the neighbor about the condition can reduce price entropy.
- The greedy plan is Monte-Carlo validated over full sampled worlds (seeded, reproducible). The validated number is the one to trust.
- If no action helps, the plan says so: "the DB cannot know more this way" is an answer, not a failure.
the verbsdecide
"Which evidence most improves the decision I actually face?"
Entropy is only the Bayes risk under log-loss. A cheap observation that halves the
entropy is worthless if it never changes what you would do — and a tiny one that flips
a high-stakes call is worth a lot. decide plans against an
objective instead of bits: the true Value of Information.
| objective | you commit to | risk is measured in |
|---|---|---|
{"kind": "entropy"} | the full posterior | bits (classical resolve) |
{"kind": "squared_error"} | the posterior mean | variance |
{"kind": "absolute_error"} | the posterior median | expected abs. error |
{"kind": "decision", "loss": [[…]], "labels": […]} | one of n decisions | expected loss |
$ nescio decide mydb --entity villa_1 --slot price \
--objective objective.json --target 10000 --actions actions.json --at 2026-07-03
DECIDE villa_1.price [absolute_error]: risk 71027.9574 abs. error now, target 10000.0000
would decide now: estimate 955000
1. call the broker (slot price, cost 5) -> expected risk 32243.8096 abs. error
2. pull the land registry (slot price, cost 40) -> expected risk 5000.9287 abs. error
would decide after: estimate 955000
total cost 45 | greedy 5001.2186 abs. error | MC-validated 4664.3089 abs. error
Read it as: "your price estimate is currently off by ±71k in expectation; a 5-unit
broker call cuts that to ±32k; adding the registry pull gets you under ±5k." The plan
also reports what it would decide before and after — if the decision would
not change, the evidence was not worth buying. Same endpoint shape as resolve:
POST /decide with objective and target.
referenceHTTP API
One process owns the database directory:
$ nescio serve mydb --port 7777 --bind 127.0.0.1
nescioDB serving mydb on http://127.0.0.1:7777 (parallel reads, exclusive writes)
All responses are JSON. Queries — including the read-only POST /resolve,
/decide and /join — run concurrently under a shared read
lock; mutations take the write lock exclusively and are durable (fsynced) before they
are acknowledged.
| route | parameters / body |
|---|---|
GET /health | — |
GET /status | slots, couplings, sources, counts |
GET /bound | entity, slot [, credible=0.95] [, at] |
GET /sample | entity [, seed=0] [, at] |
GET /certainly | entity, slot, op=gt|lt|between|is|is_not [, value] [, lo, hi] [, at] |
GET /find | slot, lo, hi [, mode=possible|certain] [, at] |
POST /join | {predicate: {op, left, right, tol?}, options?, at?} |
POST /resolve | {entity, slot, target_bits, actions, max_steps?, mc?, seed?, at?} |
POST /decide | {entity, slot, objective, target, actions, max_steps?, mc?, seed?, at?} |
POST /ingest | {entity, claim, source, at?} |
POST /ingest-batch | [{entity, claim, source, at?}, …] — one group commit, one fsync |
POST /sources | {name, reliability, half_life_days?, axiomatic?} |
POST /forget-source | {source} |
POST /recalibrate | {source, apply?, min_truth_reliability?} |
POST /priors/register | {name, slot, weights} |
POST /priors/use | {entity, slot, name} |
POST /schema/add-slot | {name, domain} — domain as in schema.json |
POST /schema/remove-slot | {name} — erases the slot's evidence and priors; refused while coupled |
POST /schema/add-value | {slot, value} — categorical slots |
POST /schema/add-coupling | {slot_a, slot_b, compat, name?} |
POST /schema/remove-coupling | {name} — by label |
GET /watches | [at] — every watch with state + knowledge horizon |
GET /watches/check | [at] — only the triggered ones |
GET /watches/events | Server-Sent Events: snapshot, then triggered / recovered |
POST /watches | {name, entity, slot, max_entropy_bits? | min_knowledge?} — returns the initial state incl. horizon |
POST /watches/remove | {name} |
at accepts "YYYY-MM-DD", a date-time, or unix seconds —
in query strings and JSON bodies alike; it defaults to now. Status codes:
| code | meaning |
|---|---|
200 | ok |
400 | invalid request — {"error": "…"} says what |
404 | no such route |
409 | axiom conflict — contradiction is a real state, not a server failure |
413 | body over 8 MiB |
The server binds to localhost by default and speaks plain HTTP without authentication — put it behind a reverse proxy if you expose it beyond the host.
referenceCLI
Every query command takes --at (time travel is a parameter, defaulting
to now) and the verbs take --json for machine-readable output.
nescio <command> --help shows every flag.
| command | |
|---|---|
init | create a database — --template NAME or --schema + --sources JSON files (formats) |
templates | list built-in templates; --show NAME prints one as JSON |
status | slots, couplings, sources, entity and record counts |
source | register or update a source (--reliability, --half-life-days, --axiomatic) |
ingest | append one claim: --interval LO..HI | --value V | --not-value V |
bound | credible region + entropy + MAP (--credible 0.95) |
sample | one consistent world, deterministic under --seed |
certainly | three-valued predicate: --gt/--lt/--between/--is/--is-not |
find | entities in a range, --mode possible|certain |
join | uncertain join, --op gt|lt|approx|same |
resolve | plan evidence to reach --target-bits, given --actions |
decide | plan evidence against an --objective, target in its risk units |
prior register / use | shared priors (--gaussian CENTER,SIGMA or --weights-file) |
schema add-slot / remove-slot / add-value / add-coupling / remove-coupling | evolve a live database — see schema evolution |
watch add / rm / list / check | standing questions with knowledge horizons — check exits 2 when triggered (watches) |
forget-source | GDPR erasure; all derived regions widen |
recalibrate | learn a source's r0 / half-life from ground truth (--apply) |
import / export | JSONL in (one group commit) / JSONL out — dates or unix seconds, see formats |
serve | the HTTP API (--port 7777, --bind 127.0.0.1) |
referenceFile formats
Every JSON shape nescio reads or writes, in one place — nothing here requires
reading source code. The quickest living reference is
nescio templates --show NAME, which prints a complete, valid
schema + sources pair for any built-in template
(real-estate, osint, sensor).
schema.json
Slots (the state spaces) and couplings (cross-slot correlations). Passed to
init --schema; stored verbatim in the database directory.
{
"slots": {
"price": {"type": "continuous", "lo": 0.0, "hi": 2000000.0, "n_bins": 200},
"condition": {"type": "categorical", "values": ["renovated", "original", "derelict"]},
"for_sale": {"type": "categorical", "values": ["true", "false"]}
},
"couplings": [
{"slot_a": "condition", "slot_b": "price", "name": "condition~price",
"compat": {"kind": "gaussian_by_category",
"centers": {"renovated": 1300000, "derelict": 500000},
"sigma": 300000}}
]
}
continuous:hi > lo,n_bins ≥ 2. Resolution is(hi − lo) / n_bins— 200 bins over 2M means 10k steps.categorical: at least two distinct values. Booleans are the two-value domain["true", "false"].couplingsis optional;namedefaults to"slot_a~slot_b"and must be unique (it is the handle forschema remove-coupling).
Coupling kinds
Slot order matters — each kind expects specific domain kinds on
slot_a and slot_b:
| kind | slot_a | slot_b | fields | semantics |
|---|---|---|---|---|
gaussian_by_category |
categorical | continuous | centers: {value: center}, sigma |
each category pulls slot_b toward its center, Gaussian-shaped; a category without a center is uninformative |
step_threshold |
continuous | categorical | threshold, below: {value: w}, above: {value: w} |
category weights switch at the threshold; missing entries are 1 (uninformative) |
matrix |
categorical | categorical | weights: {a: {b: w}}, default (1) |
pairwise compatibility; missing entries fall back to default |
table |
any | any | rows (na × nb) |
fully explicit factor table, rows over slot_a's cells — maximal control, but
fixed dimensions (it blocks schema add-value on its slots) |
All weights must be finite and ≥ 0. A weight of 0 is a hard constraint — combined with an axiom it can produce a query-time conflict, exactly like contradicting axioms.
sources.json
What init --sources accepts — a JSON array, or the name-keyed map
nescio itself writes into the database directory. Both work, so a file copied from an
existing database is valid input.
[
{"name": "land_registry", "reliability": 1.0, "axiomatic": true},
{"name": "broker", "reliability": 0.85, "half_life_days": 90.0},
{"name": "web_scraper", "reliability": 0.7, "half_life_days": 45.0}
]
reliability∈ (0, 1]. Non-axiomatic sources are capped just below 1.0 internally — only axioms may be absolute.half_life_daysoptional, > 0; omitted means no decay.axiomaticoptional, default false.
Evidence records (JSONL for import / export)
One record per line. observed_at takes unix seconds or a
readable date — and at is accepted as an alias, so hand-written files
look like the CLI and the HTTP body. export always writes canonical
unix seconds.
{"entity": "poi_7", "claim": {"type": "value", "slot": "role", "value": "courier"}, "source": "informant", "at": "2026-06-20"}
{"entity": "poi_7", "claim": {"type": "interval", "slot": "age", "lo": 25, "hi": 35}, "source": "social_media", "observed_at": "2026-06-28T14:30"}
{"entity": "poi_9", "claim": {"type": "not_value", "slot": "role", "value": "financier"}, "source": "news_wire", "observed_at": 1782345600}
Claim types: interval (continuous; lo ≤ hi, must
intersect the domain), value and not_value (categorical;
the value must be in the domain). The source must be registered before import.
Actions (resolve / decide)
A JSON array describing what evidence could be bought. The
source is inline — it does not need to be registered, so you can plan
with hypothetical sources.
[
{"name": "call the broker", "slot": "price", "cost": 5,
"source": {"name": "broker", "reliability": 0.85, "half_life_days": 90},
"answer_width": 100000}
]
| field | |
|---|---|
name | label used in the plan output |
slot | which slot the answer lands on — need not be the target slot; couplings carry the gain across |
cost | whatever unit you optimize (money, minutes, API calls) |
source | an inline source object: reliability, half-life, axiomatic |
answer_width | continuous slots only: the width of the interval the answer would arrive as |
Objectives for decide are documented at the verb:
{"kind": "entropy" | "squared_error" | "absolute_error"} or
{"kind": "decision", "loss": [[…]], "labels": […]} with one loss row per
decision and one entry per cell of the target slot.
priors.json
Managed via nescio prior register / use — you rarely write it by
hand, but it is plain JSON:
{
"registry": {"district_9": {"slot": "price", "weights": [0.2, 0.4, "…one per cell…"]}},
"assignments": {"villa_2": {"price": "district_9"}},
"defaults": {"price": [1.0, 1.0, "…applies to every unassigned entity…"]}
}
Weights are relative (they are normalized), must be finite and ≥ 0, not all zero,
and their length must equal the slot's cell count (n_bins or the number
of categorical values).
watches.json
Standing questions (watches), written by
nescio watch add / POST /watches — you rarely edit this by
hand, but it is plain JSON like everything else:
[
{"name": "price_fresh", "entity": "villa_1", "slot": "price", "max_entropy_bits": 5.0},
{"name": "sell_known", "entity": "villa_1", "slot": "wants_to_sell", "min_knowledge": 0.3}
]
Exactly one of max_entropy_bits (bits) / min_knowledge
(ratio in (0, 1]) per watch; names are unique, the slot must exist. The file is
optional — no watches, no file.
referenceClient libraries
Typed clients for Python, TypeScript and Java. All three are zero-dependency by design and single-file vendorable: if you'd rather not add a package, copy the one source file into your project and you are done.
# pip install nesciodb (Python 3.9+, urllib only — or vendor the one file)
from datetime import date
from nesciodb import NescioClient, claim, source, action, objective
db = NescioClient("http://localhost:7777")
db.ingest("villa_1", claim.interval("price", 900_000, 1_000_000),
"broker", at="2026-06-25")
b = db.bound("villa_1", "price", at=date(2026, 7, 3))
print(f"{b.entropy_bits:.2f} bits, knowledge {b.knowledge_ratio:.0%}")
print(b.region) # [(570000.0, 1210000.0)]
# Entity handles for entity-centric code:
villa = db.entity("villa_1")
villa.certainly("price", lt=1_500_000) # "true" | "possible" | "false"
# Value of Information: which evidence most improves the decision?
plan = villa.decide("price", objective=objective.absolute_error(), target=10_000,
actions=[action("pull the land registry", "price", cost=40,
src=source("land_registry", 1.0, axiomatic=True),
answer_width=20_000)])
print(plan.recommended_now, "->", plan.validated_risk)
# Watches: fire when knowledge decays past a threshold
st = db.add_watch("price_fresh", "villa_1", "price", max_entropy_bits=5.0)
print(st.horizon_date) # the knowledge horizon: "2026-08-10"
for ev in db.watch_events(): # SSE: snapshot, then transitions
if ev.event == "triggered":
notify(ev.state)
// npm install nesciodb (Node 18+ or any modern browser — or vendor the one file)
import { NescioClient, claim } from "nesciodb";
const db = new NescioClient("http://localhost:7777");
await db.ingest("villa_1", claim.interval("price", 900_000, 1_000_000), "broker", {
at: "2026-06-25",
});
const b = await db.bound("villa_1", "price", { at: new Date("2026-07-03") });
console.log(b.entropyBits, b.knowledgeRatio, b.region);
// Entity handles for entity-centric code:
const villa = db.entity("villa_1");
await villa.certainly("price", { op: "lt", value: 1_500_000 });
// Value of Information: which evidence most improves the decision?
const plan = await villa.decide({
slot: "price",
objective: { kind: "absolute_error" },
target: 10_000,
actions: [{
name: "pull the land registry", slot: "price", cost: 40,
source: { name: "land_registry", reliability: 1.0, axiomatic: true },
answerWidth: 20_000,
}],
});
console.log(plan.recommendedNow, "->", plan.validatedRisk);
// Watches: fire when knowledge decays past a threshold
const st = await db.addWatch({ name: "price_fresh", entity: "villa_1",
slot: "price", maxEntropyBits: 5.0 });
console.log(st.horizonDate); // the knowledge horizon: "2026-08-10"
for await (const ev of db.watchEvents()) { // SSE: snapshot, then transitions
if (ev.event === "triggered") notify(ev.state);
}
// JDK 17+; drop the three files into your project, or mvn package
import dev.nescio.NescioClient;
import dev.nescio.NescioClient.*;
var db = new NescioClient("http://localhost:7777");
db.ingest("villa_1", Claim.interval("price", 900_000, 1_000_000), "broker",
At.date("2026-06-25"));
Bound b = db.bound("villa_1", "price", At.date("2026-07-03"));
System.out.printf("%.2f bits, knowledge %.0f%%%n",
b.entropyBits(), b.knowledgeRatio() * 100);
What each client covers:
| client | verbs | times (at) | errors |
|---|---|---|---|
| Python | all — incl. decide, schema evolution, watches + SSE stream, batch ingest, priors, recalibrate |
ISO string, unix seconds, date, datetime |
NescioError with .status |
| TypeScript | all — incl. decide, schema evolution, watches + SSE stream, batch ingest, priors, recalibrate |
ISO string, unix seconds, Date |
NescioError with .status |
| Java | bound … resolve, ingest, sources, priors, recalibrate |
At.date(…), At.unix(…) |
NescioException with .status() |
Python and TypeScript also ship builders for every wire format —
claim.*, domain.*, coupling.*,
objective.* — so the file-format rules (slot
order, field names) are encoded in the type system instead of your memory.
referenceStorage & ops
A database is a directory. Config is human-readable JSON; the evidence log is a compact append-only binary (~2.6× smaller than JSONL, no parse cost on replay):
mydb/
schema.json slots and couplings
sources.json reliability, half-life, axiomatic
priors.json shared priors
watches.json standing questions (only if you add some)
log.bin the evidence log (append-only binary)
The binary log is never the only copy of the truth you can read —
export reconstructs JSONL any time, and import goes the
other way with a single group commit:
$ nescio export mydb | head -1
{"entity":"villa_1","claim":{"type":"interval","slot":"price","lo":900000.0,"hi":1000000.0},"source":"broker","observed_at":1782345600}
Performance
Measured on an M-series MacBook, 200,000 entities / 400,000 evidence records
(cargo run --release --example bench):
ingest (group commit, one fsync) ~1.1M records/s
open / log replay ~1.2M records/s
bound 4.5 µs (8.6 µs with couplings)
resolve < 1 ms
Every write is durable before it is acknowledged — a batch of any size costs one fsync.