Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Release crates.io Docs

This site is the code-derived documentation for Nibli: a zero-hallucination symbolic reasoning engine with a human-readable knowledge-representation surface (nibli KR).

These pages track main, not a released tag — they are rebuilt on every push that touches mdbook/. The badges above show the latest release and the current published crate version; where the two could differ, the repository is authoritative. Released API docs are versioned on docs.rs.

It is not the Orange AVA book manuscript. Book rights are reserved by the publisher. Nothing here is copied from that manuscript. Claims should re-derive from the repository: source code, tests, just recipes, shipped corpora (.nibli files), and the root engine specifications.

Where to run things

SurfaceURL / command
Interactive playgrounddhilipsiva.dev/nibli-playground
Docs (GitHub Pages mirror)dhilipsiva.github.io/nibli
Docs (site integration, planned)dhilipsiva.dev/docs/nibli/ — pending the site-repo copy (DEPLOY.md §2b)
Local buildjust docs / just docs-serve (inside nix develop)
Rust APIdocs.rs/nibli-engine — every published crate is indexed in the API index; locally cargo doc -p <crate> --open

Primary host path is /docs/nibli/; the GitHub Pages project site uses base path /nibli/. CI builds the mirror with site-url=/nibli/. Prefer relative links inside chapters so both bases work.

Open the playground →

Source layout

PathRole
mdbook/src/Hand-authored pages (this site)
mdbook/book/Generated HTML only — do not edit
Repo root NIBLI_KR.md, LOGIC_IR.md, GUARANTEES.md, …Normative engine specs (linked from Reference)
book/Private manuscript checkout — never imported into this tree

Open work — engine, tooling and docs alike — is tracked in TODO.md at the repository root.

Start here

  1. What Nibli guarantees — the four-valued contract and scope.
  2. Quickstart — Nix + just run.
  3. nibli KR cookbook — surface syntax cheat sheet.
  4. Playground — browser triad without installing anything.
  5. Authorization — builtin policy and multi-language can / field masks.

User guide — overview

Audience: people writing .nibli knowledge bases, using the REPL or playground, or embedding the engine.

PageWhat it covers
What Nibli guaranteesFour-valued verdicts, closed world/domain, trusted compute
QuickstartNix dev shell, just run, first claims
nibli KR cookbookSurface syntax stubs + link to the full spec
PlaygroundHosted triad UI and Formalize
GDPR walkthroughA worked compliance KB — engine-checked verdicts and the consent-withdrawal flip
Drug-interactions walkthroughA worked safety KB — the three-step mechanism and its negative controls
Belief revision:retract, retract ≡ never-asserted, and edit-and-re-query in the playground
AuthorizationBuiltin policy, can / fields / explain, Rust + Python adapters

Deeper sources (repo root)

TopicWhere
Product overviewREADME.md
Formal contractsGUARANTEES.md
Language (normative)NIBLI_KR.md
Example corporagdpr.nibli, drug-interactions.nibli, readme.nibli
Host / WASM ship pathDEPLOY.md

Query model: state a claim to check for entailment (e.g. dog(Adam).), not an interrogative. The playground’s decorative ? is not sent to the engine.

The developer guide covers the crate map, the IR, the WASM/host/compute path, the CI gates, and the WIT surface; published crate APIs are in the API index. Remaining docs work is tracked in TODO.md.

What Nibli guarantees

Derived from the engine README and GUARANTEES.md. The full contract text lives in those files; this page is a short operator summary.

One surface language

Nibli has one front-end language: nibli KR (predicate-call surface: dog(Adam)., animal(every dog).). Name resolution is fail-closed: an unknown word is a compile error, never an arity-guessed new predicate. Normative spec: NIBLI_KR.md.

Soundness (relative to what you asserted)

The engine never returns TRUE for a formula that does not follow from the asserted facts and compiled rules, given a correct implementation. A TRUE answer comes with a formal proof trace. Bugs would be deterministic and testable — not stochastic fabrication.

This is not omniscience: change the premises and the verdict can change.

Closed world and closed domain

Inference assumes:

  • Closed world — a fact you did not assert is taken to be false, not unknown.
  • Closed domain — quantifiers range only over entities the knowledge base knows.

Four-valued outcomes

How to read a query result (product README wording):

VerdictMeaning
TRUEA proof exists from your premises (facts + rules + trusted backend results).
FALSENot derivable from those premises. This is not a proof of ¬P.
UNKNOWNThe search could not decide (e.g. a cycle, incomplete knowledge, or negation over an undecided sub-goal).
RESOURCE_EXCEEDEDA budget ran out before the search finished — depth, fuel, or memory. Not a verdict about the claim: raise the budget and re-run.

All four are QueryResult variants in the engine itself, not host conventions — RESOURCE_EXCEEDED carries which limit was hit. Raise them with the NIBLI_FUEL / NIBLI_MEMORY_MB env vars or the :fuel / :memory REPL commands; see GUARANTEES.md.

Trusted compute backend

Results from the external compute backend (exponential, logarithm, or predicates you register) are a trusted oracle, not a derivation: a true reply is auto-asserted mid-query. Built-in arithmetic (product / sum / quotient) is local. Any conclusion that passes through the backend is only as sound as that oracle.

Where the full story lives

  • GUARANTEES.md — differential oracles (Vampire / clingo), Lean proofs, determinism, mutation baseline.
  • LOGIC_IR.md — the FOL intermediate form the reasoner consumes.
  • CI: just ci, just verify-soundness, just verify-proofs.

Quickstart

Source: README — Getting Started and the root Justfile.

Prerequisites

  • Nix — rustc, cargo-component, just, wasmtime, mdbook, and the rest come from flake.nix.

Enter the shell and run the REPL

# From the nibli repository root
nix --extra-experimental-features 'nix-command flakes' develop

# Build the pipeline component + native host and launch the REPL
just run

just run is the full local operator path (WASM component + nibli-host). For a fast native check only:

just test          # unit tests (lib)
just check         # type-check workspace

First claims

In the REPL, assert facts and rules, then query by stating a claim (not asking a question):

animal(every dog).
dog(Adam).

Then query:

animal(Adam).

Expect TRUE with a proof when the rule and fact support the claim. See What Nibli guarantees for how to read FALSE vs UNKNOWN.

A larger starter table lives in the nibli KR cookbook. The repo root also ships example files such as readme.nibli, gdpr.nibli, and drug-interactions.nibli (:load path in the host REPL) — worked tours in the GDPR walkthrough and the drug-interactions walkthrough.

Dictionary note

The vocabulary is committed Rust source (nibli-lexicon/src/corpus/) — no network fetch at build or runtime. Local, CI, and the hosted playground share the same corpus.

Docs site (this book)

just docs          # build → mdbook/book/
just docs-serve    # http://127.0.0.1:3000

Prefer not to install?

Use the hosted playground — the engine runs fully in the browser.

nibli KR cookbook

Short surface cheat sheet. Normative reference: NIBLI_KR.md (v0.1). Executable grammar: nibli-kr/src/nibli_kr.pest. Examples match the product README language table.

nibli KR is a strict predicate-call surface: one statement per line, ending with a period. Unknown predicate words are a compile error, never a guess — names resolve through the committed English corpus, fail-closed.

Common patterns

nibli KRReads as
dog(Adam).Adam is a dog
animal(every dog).every dog is an animal (a rule)
~eats(Adam).Adam does not eat
past eats(me, some food).I ate some food
dog(Adam) & cat(Betis).conjunction (| or, -> if-then)
goes(Adam, destination: some market).named argument places
beautiful(every person where ~cat).rule with a negated restrictor (NAF)
Kim = Adam.identity
red(exactly 2 red).exact-count claim
all $x: dangerous($x) & uses(Adam, $x) -> warns($x).prenex rule with variables

Predicates and places

dog(Adam).
goes(Adam, destination: some market).
  • Positional args fill places in order; named args use corpus place labels (destination:, …).
  • Converted aliases and compounds are dictionary-driven; uncurated a+b compounds fail closed.

Rules

animal(every dog).
eats(every animal).
dog(Adam).

Description-style universals (every dog) compile to rules. Explicit prenex:

all $x: dog($x) -> animal($x).

Negation-as-failure

~eats(Adam).

Stratified NAF under closed-world assumptions — see GUARANTEES for oracle coverage. Unstratifiable programs are rejected at assert time.

Where to go next

Playground

The Transparency Triad UI runs the full pipeline (nibli-kr → nibli-semantics → nibli-reason) in the browser. There is no nibli server.

Open playground →

Local equivalent: just ui (Dioxus on port 8080). Ship path: DEPLOY.md.

Panes

PaneRole
SourcePlain English (optional Formalize input)
nibli KRThe knowledge base — formal claims the engine asserts
Back-translationStructure-exposing gloss of the KR

The nibli KR pane is the knowledge base. Each query rebuilds a fresh engine, re-asserts the KB, then runs the claim.

Query model

State a claim, do not ask a question:

eats(Adam).

The UI may show a decorative ? next to the query box — it is not part of the text sent to the engine. Verdicts are TRUE / FALSE / UNKNOWN (see guarantees).

Formalize (optional)

Formalize (not “compile”) is a bring-your-own-key LLM step from the Source tab. The key stays in tab memory only; the request goes from your browser to the provider you choose. Drafts are checked by the real nibli-kr + nibli-semantics + render round-trip gates before they land in the KR pane. Formalize sits outside the deterministic reasoning core — always review the KR and back-translation.

Example knowledge bases (preset hooks)

The header dropdown loads preloaded KBs used in regression tests and demos. Treat them as example corpora, not as chapters of any third-party book. In example mode the KR is read-only and Formalize is disabled; the query control becomes a preset list that auto-runs.

The dropdown names and preset labels below are byte-stable hooks — they are defined in nibli-ui/src/examples.rs, pinned by the shipped_examples_compile guard (just test-ui), and safe to reference from docs and links:

Dropdown nameCorpusPreset queries
Syllogism (Ch 18)inline 3-line KBdoes Adam eat?—a 2-hop proof · is Adam an animal?—1 hop · is Adam a bird?—a real FALSE
GDPR compliance (Ch 19)gdpr.nibli — see the GDPR walkthroughlawful basis? (Art 6) · right to erasure? (Art 17) · a controller is not a consenting person—exhaustive FALSE · health record → personal data (Art 4/9, derived)
Constitutional core (utopia)utopia.nibli — extra playground corpus, not a book chapter14 presets over the constitutional scenario (floor duties, voiding multi-sig, imprisonment routing, whistleblower shield)
Drug interactions (Ch 20)drug-interactions.nibli — see the drug-interactions walkthroughconcentration rising? · toxicity risk? · safety alert?—a 3-hop proof · negative control—no alert

The GDPR, utopia, and drug KBs are include_str!-ed from the same repo-root .nibli files the engine’s regression tests pin, so the playground cannot drift from the tested corpora.

Belief revision: edit and re-query

Each query rebuilds a fresh engine from the KR pane, so revising the KB is just editing it: delete or #-comment a fact line and re-run the claim. Presets are read-only — to revise one, paste its corpus into Custom mode first. Worked demos: Belief revision.

Built-in vs external compute

In-browser: built-in arithmetic (product / sum / quotient) and ground numeric comparisons work locally. External compute backend predicates need the host + backend path (just run-with-backend) — not the pure playground.

More

GDPR walkthrough

A worked compliance knowledge base: a formalizable slice of the EU General Data Protection Regulation (Articles 5, 6, 7, 9, 15, 17, 33), answered by deterministic, auditable deduction.

Source of truth: gdpr.nibli at the repo root. Every verdict quoted below is pinned by the gdpr_* regression tests in nibli-engine/tests/integration.rs, so this page cannot silently drift from the engine. The same corpus ships as the playground preset “GDPR compliance (Ch 19)” — an example KB, not a chapter of any third-party book.

The scenario

EntityRole
AdamData subject; has given consent
AkmesController that suffered a breach
GugliController with no breach (clean control)
KanrekAdam’s health record (special-category data, Art 9)
OrdrekAn ordinary personal-data record

Second-order legal concepts are mapped onto proxy predicates, disclosed in the corpus header:

PredicateReads as
permitted(x)processing of x has a lawful basis (Art 6)
~permitted(x)no lawful basis remains → right to erasure (Art 17)
obligated_by(x, event { P() })x is under a legal obligation that P
permitted(x, event { P() })x has the right that P

Load it in the REPL

:load gdpr.nibli
[Load] Done: 24 asserted, 77 skipped, 0 errors

The 77 skipped lines are comments and blanks. Fact ids are assigned in file order; the consent fact approves(Adam). lands as fact #21 — the corpus deliberately defines the Article 17 rule after the scenario so these ids stay stable (a rule’s position in the file never affects reasoning).

Engine-checked queries

Query by stating a claim with the ? prefix. All verdicts below are asserted by the regression suite against the full loaded corpus:

ClaimVerdictWhy
? permitted(Adam).TRUEConsent is a lawful basis (Art 6(1)(a))
? ~permitted(Adam).FALSEA lawful basis stands, so no erasure right
? permitted(Gugli).FALSEA controller is not a consenting subject — an exhaustive, deduced FALSE
? data(Kanrek).TRUEHealth record → personal data, derived via data(every healthy data).
? obligated_by(Kanrek, event { correct() }).TRUEArt 5 accuracy reaches health data through the category chain
? obligated_by(Kanrek, event { exact() }).TRUESpecial-category data needs a stricter basis (Art 9)
? obligated_by(Ordrek, event { exact() }).FALSEOrdinary data does not
? permitted(Adam, event { data discovers() }).TRUERight of access / DSAR (Art 15)
? permitted(Akmes, event { data discovers() }).FALSEA controller does not acquire the subject’s access right
? obligated_by(Akmes, event { message() }).TRUEBreached controller must notify (Art 33)
? obligated_by(Gugli, event { message() }).FALSENo breach, no notification duty
? obligated_by(Adam, event { removes() }).FALSEConsent present → no erasure obligation (Art 17 rule)

Every FALSE here is a deduced false under the closed-world assumption — the engine exhausted the search — not a shrug. ? also prints a plain-English [Why] summary and the proof tree; see What Nibli guarantees for the verdict contract.

The headline demo. Adam’s only lawful basis is consent (fact #21). Retract it and re-query — both verdicts flip:

:retract 21
[Retract] Fact #21 retracted. KB rebuilt.

? permitted(Adam).
[Query] FALSE

? ~permitted(Adam).
[Query] TRUE

No lawful basis remains, so the right to erasure (Art 17(1)(b)) arises. The erasure verdict is derived by negation-as-failure and its proof carries the naf_dependent flag — the engine discloses that the conclusion rests on an absence. Nothing was edited by hand: the same rules, re-derived over the surviving facts. See Belief revision for the mechanics.

The corpus also stores Article 17 as a rule with a negated restrictor:

obligated_by(every person where ~approves, event { removes() }).

where ~approves compiles to a negation-as-failure check per subject: a consenting person carries no erasure obligation while a non-consenting one does (pinned by gdpr_erasure_rule_is_per_subject and the rule-level belief-revision test).

Honest boundaries

Two scope decisions the corpus makes explicitly, in its own comments:

  • Art 7 (“freely given” consent) is deliberately not encoded. Whether a consent was un-coerced is a case-by-case human judgment — it stays outside the deductive firewall rather than being faked as a rule.
  • The erasure rule keys on ~approves, not ~permitted. The corpus derives a lawful basis from a legal obligation, so a where ~permitted rule would close a negative basis↔obligation cycle — the engine correctly rejects that as unstratifiable at assert time. For Adam, whose only basis is consent, the two formulations coincide, which is why the right can equivalently be queried as ~permitted(Adam).

Try it in the playground

Select “GDPR compliance (Ch 19)” in the playground header dropdown. Its preset queries are exactly the first four claims above: lawful basis? (Art 6) · right to erasure? (Art 17) · a controller is not a consenting person—exhaustive FALSE · health record → personal data (Art 4/9, derived). Proofs render with the curated legal-domain overlay (“has a lawful basis for processing”), never a bare variable.

Drug-interactions walkthrough

A worked safety knowledge base: pharmacokinetic drug-drug interaction (DDI) reasoning — “should this co-prescription raise a safety alert?” — decided by deduction over an explicit mechanism instead of a statistical guess.

Source of truth: drug-interactions.nibli at the repo root. Every verdict quoted below is pinned by the ddi_* regression tests in nibli-engine/tests/integration.rs. The same corpus ships as the playground preset “Drug interactions (Ch 20)” — an example KB, not a chapter of any third-party book.

The scenario

The warfarin + fluconazole interaction, mediated by the CYP2C9 enzyme: fluconazole inhibits CYP2C9; warfarin (narrow therapeutic index) is metabolised by CYP2C9, so its concentration rises → toxicity risk → safety alert. Apixaban is the negative control: metabolised by CYP3A4, which fluconazole does not inhibit → no alert, as a real deduced FALSE. Phenytoin is the second control: pharmacologically at risk, but not on the patient’s chart.

The corpus has no native pharmacology vocabulary, so it maps onto the nearest committed relations and discloses the mapping in its header:

PredicateReads as
chemical(d)d is a drug
uses(p, d)patient p takes drug d
prevents(d, e)drug d inhibits enzyme e
metabolized_by(d, e)drug d is metabolised by enzyme e
thin(d)narrow therapeutic index
increases(d)blood concentration is raised
dangerous(d)at toxicity risk
warns(d)warrants a safety alert

Enzymes are opaque rigid Names: Siptucin = CYP2C9, Sipcivon = CYP3A4.

The three-step mechanism

Step 1 — concentration rise. Grounded conditionals per affected substrate (the fully general join rule all $a, $b, $e: prevents($a,$e) & metabolized_by($b,$e) -> increases($b). also compiles and reasons correctly — the grounded form is an encoding choice, not a limitation):

prevents(Flukonazol, Siptucin) & metabolized_by(Varfarin, Siptucin) -> increases(Varfarin).

Step 2 — toxicity risk. One general rule with a conjunctive restrictor; both conditions are required:

dangerous(every chemical where increases where thin).

A wide-margin drug whose concentration rises is not flagged, and a narrow-index drug with no interaction is not flagged (both negative controls are pinned by ddi_toxicity_requires_both_conditions).

Step 3 — the alert is patient-gated. Risk is drug-level pharmacology; the actionable alert only fires for an at-risk drug this patient actually takes:

all $da: dangerous($da) & uses(Adam, $da) -> warns($da).

Load it in the REPL

:load drug-interactions.nibli
[Load] Done: 16 asserted, 78 skipped, 0 errors

Two fact ids matter for the belief-revision demos below (assigned in file order, pinned by ddi_corpus_transcript_pins): the inhibition fact prevents(Flukonazol, Siptucin). is #4, and the regimen fact uses(Adam, Varfarin). is #10.

Engine-checked queries

ClaimVerdictWhy
? increases(Varfarin).TRUEStep 1: inhibited enzyme + substrate
? dangerous(Varfarin).TRUEStep 2: raised concentration + narrow index
? warns(Varfarin).TRUEStep 3: at risk and on Adam’s chart — a 3-hop proof
? increases(Apiksaban).FALSECYP3A4 is not inhibited by fluconazole
? warns(Apiksaban).FALSEThe negative control: a deduced FALSE, not unknown
? increases(Fenitoin).TRUESame shared inhibitor, same general rules
? dangerous(Fenitoin).TRUERisk is drug-level — no per-drug rule needed
? warns(Fenitoin).FALSEBut Adam does not take it: the regimen gate

The phenytoin pair is the point of step 3: pharmacological risk is general, the actionable alert is patient-specific.

Witness extraction (??) enumerates bindings instead of checking one claim — “which drugs are CYP2C9 substrates?”:

?? metabolized_by($da, Siptucin).

lists warfarin and phenytoin as witnesses for $da; apixaban (a CYP3A4 substrate) does not appear (pinned by ddi_witness_cyp2c9_substrates).

Belief revision: two clinical moves

Alerts are never baked in — they are re-derived from current facts, so the two canonical chart edits are single retractions (see Belief revision):

Discontinue the inhibitor (retract prevents(Flukonazol, Siptucin)., #4): the mechanism’s entry premise disappears, so the concentration rise, the toxicity risk, and the alert all dissolve in one step — for both substrates, since they share the inhibitor:

:retract 4
[Retract] Fact #4 retracted. KB rebuilt.

? warns(Varfarin).
[Query] FALSE

? dangerous(Fenitoin).
[Query] FALSE

Discontinue the drug (retract uses(Adam, Varfarin)., #10): the alert is withdrawn while the drug-level risk stays derivable — the alert is gated on the regimen, the risk is not:

:retract 10
[Retract] Fact #10 retracted. KB rebuilt.

? dangerous(Varfarin).
[Query] TRUE

? warns(Varfarin).
[Query] FALSE

Both moves are pinned by ddi_belief_revision_discontinue_inhibitor and ddi_belief_revision_discontinue_drug.

Try it in the playground

Select “Drug interactions (Ch 20)” in the playground header dropdown. Its presets are the headline chain plus the negative control: concentration rising? · toxicity risk? · safety alert?—a 3-hop proof · negative control—no alert. Proofs render with the curated pharmacology overlay (“fluconazole inhibits CYP2C9”, “warfarin is at toxicity risk”), never a bare variable or a raw transliterated name.

Belief revision

Nibli’s conclusions are never stored — they are re-derived from the current facts on every query. Retract a fact and everything that depended on it dissolves; nothing has to be manually un-concluded. This page covers the mechanics on both runtime surfaces.

Sources: GUARANTEES — Retraction Model, nibli-host REPL (nibli-host/src/main.rs), and the retraction metamorphic differential (nibli-verify/src/retract_diff.rs, part of just verify-soundness).

The guarantee: retract ≡ never-asserted

Retraction has one path — rebuild: the fact’s registry record is marked retracted and the knowledge base is rebuilt by replaying the surviving records. Equivalence classes, indexes, and the quantifier domain are all re-derived, so a KB after a retraction answers byte-identically to a fresh engine that never saw the fact.

This is not just documented — it is checked metamorphically at scale: the retraction differential generates seeded random programs mixing ground facts, rules, identity links, and stratified negation, retracts random earlier statements, and requires the engine to agree with a never-asserted twin on a battery of ground and quantified queries after every retraction.

In the host REPL

Facts get ids at assert time ([Fact #N] …, in file order under :load). List and retract by id:

:facts
[Facts] 24 active fact(s):
...

:retract 21
[Retract] Fact #21 retracted. KB rebuilt.

Re-query and the verdicts reflect the surviving facts only. With the durable store attached, the retraction persists as a tombstone — provenance is kept, and a replay never resurrects the fact.

Two worked, engine-checked demos:

  • GDPR walkthrough — withdraw consent (:retract 21): the lawful basis flips FALSE and the right to erasure flips TRUE.
  • Drug-interactions walkthrough — discontinue the inhibitor (:retract 4) or the drug (:retract 10): the alert dissolves; in the second case the drug-level risk deliberately stays TRUE.

Verdicts that rest on an absence

Retraction interacts with negation-as-failure: a claim like “no lawful basis remains” becomes TRUE precisely because a search found nothing. The engine discloses this — such proofs carry the naf_dependent flag, and query results distinguish a deduced FALSE from UNKNOWN (naf-dependent) states. See What Nibli guarantees.

In the playground: edit and re-query

The browser playground has no :retract command — it does not need one. The nibli KR pane is the knowledge base, and every query rebuilds a fresh engine from that pane, so belief revision is edit-and-re-query: delete (or #-comment) the fact’s line, run the claim again, and the verdict reflects the edited KB. That is the same “retract ≡ never-asserted” semantics, achieved by construction.

The preset examples load read-only. To revise one, switch to Custom mode and paste the corpus in from the repo root (e.g. gdpr.nibli, drug-interactions.nibli# comment lines are skipped at assert time), then comment out approves(Adam). or uses(Adam, Varfarin). and re-run the preset claims from the walkthroughs.

Authorization

Nibli ships a built-in authorization surface: a versioned KR policy, fail-closed vocabulary, and a warm Authorizer that answers with ordinary entailment. Allow means the engine returned TRUE for authorized(...) — not a learned score, not an LLM.

Code lives in nibli-auth (native), nibli:engine@0.7.0 authorizer (WASM component export), optional Rust features (axum, async-graphql, juniper), and nibli-auth-py / python/nibli_auth (PyO3).

This page is code-derived (crates, policy file, examples). It is not the Orange AVA book manuscript.

Ontology

PredicatePlacesRole
authorizedagent, action, resourceDecision head — “may perform action on resource”
ownsowner, owned, basisExisting corpus; ownership facts
has_roleagent, roleRole assignment
in_tenantagent, tenantTenancy of a principal
resourceresourceDeclare a protected resource (needed for admin rules)
resource_tenantresource, tenantResource’s tenant
visible_attragent, resource, attrField-level (reserved for finer policies; v0.1 masking uses row can)
agentagentOptional sort for principals

Do not use as auth heads (corpus collisions):

NameWhy
canTin/can (lante), not permission
fieldAgricultural field (foldi)
principal“Chief among” (ralju) — prefer agent

Actions, roles, attrs are KR quoted strings: "read", "update", "admin", "title".

Entities (Agents & Resources) can be specified as:

  • Capitalized Names: Alice, Doc1, User1
  • Quoted Strings: "1", "2", "user_123" (required when IDs are numeric strings or lowercase).
owns(Alice, Doc1).                  # Capitalized name constants
owns("101", "202").                 # Quoted string IDs
authorized(Alice, "update", Doc1).   # query shape
has_role(Carol, "admin").
resource(Doc1).

Fail-Closed Lexicon & Custom Domain Roles (NIBLI_KR §13)

Nibli enforces a fail-closed vocabulary: uncurated predicate names (e.g., has_codename, program_role) are rejected at compile time rather than silently defaulting to an arity-2 guess. This prevents security bugs caused by typos (e.g., is_admim).

To model custom roles, permissions, or domain attributes without changing Rust code:

  • Use has_role(Agent, "custom_permission_or_codename") with quoted string tokens (e.g., "read_application", "create_invoice"). Quoted string tokens can be any arbitrary string.
  • Use in_tenant(Agent, "group_id") for custom tenancy/organization groups.
has_role(Alice, "read_application").
in_tenant(Alice, "org_sales_east").

Builtin policy

File: nibli-auth/policy/auth-0.1.0.nibli
Version constant: nibli_auth::POLICY_VERSION ("0.1.0").

Shipped rules (summary):

  • Ownerowns($a, $r) ⇒ authorized for "read" and "update".
  • Adminhas_role($a, "admin") & resource($r) ⇒ read/update that resource.
  • Tenant — same tenant on agent and resource ⇒ "read" only.

Rule writing tip: every variable that appears in the conclusion should be constrained by a positive body condition (or bound by a ground query). Free conclusion-only variables do not fire under backward chaining here (admin rules therefore mention resource($r)).

Overlay extra KR with Authorizer::load_policy(Some(extra)) or per-call context_kr.

Efficiency model

RequirementMechanism
No engine per requestOne warm authorizer (or one per OS thread)
Policy onceload_policy / thread-local first use
Ephemeral request factscontext_kr asserted then retracted
Cheap hot pathcan / can_any / allowed_fieldsno proof
Batch authorizationcan_any — derives roles/tenants once per batch over context
Proofs opt-inexplain only
Decision cacheKeyed by policy version + agent/action/object + context hash

Multi-threaded servers: the KB uses RefCell and is !Send. Do not put Authorizer in axum::State or Arc<Mutex<_>>. Use:

  • Rust: nibli_auth::tls (thread-local warm policy).
  • Python: same TLS behind nibli_auth_native (each OS thread warms once).

Cross-thread durable owns/roles belong in the app database and should be passed as context_kr each call (see the examples).

UNKNOWN on the hot path is treated as deny (allowed=false); only engine TRUE allows.

Rust API

#![allow(unused)]
fn main() {
use nibli_auth::{Authorizer, tls};

let mut auth = Authorizer::new();
auth.load_policy(None)?;
auth.assert_facts("owns(Alice, Doc1).")?;
let d = auth.can("Alice", "update", "Doc1", "")?;
assert!(d.allowed);

// Multi-thread / axum workers:
let d = tls::can("Alice", "read", "Doc1", "owns(Alice, Doc1).")?;
let batch = tls::can_any("Alice", "read", &["Doc1", "Doc2"], "owns(Alice, Doc1).")?;
let fields = tls::allowed_fields("Alice", "read", "Doc1", "owns(Alice, Doc1).", &["title", "body"])?;
}
  • can — row-level allow/deny (Decision).
  • can_any — batch queryset/row evaluation returning Vec<(String, bool)>.
  • allowed_fields — v0.1: if row can allows, return all candidates (serializer masking).
  • explain — same as can plus optional proof JSON.

Optional Cargo features on nibli-auth: axum (Agent header extractor, require, can_any, field_mask), async-graphql, juniper.

WIT (WASM component)

Package nibli:engine@0.7.0 exports authorizer alongside engine (logical auth surface v0.1 in the single shipping package).

WITNotes
load-policyBuiltin (+ optional extra KR)
can / can-any / allowed-fields / explainSame semantics as Rust
Parameter objectProtected resource id — WIT reserves the keyword resource

Native and guest both wrap the same policy file via nibli-auth. engine.session and authorizer.session do not share a KB in v0.1.

Python API

just build-auth-py    # maturin develop into .venv-auth
just test-auth-py
from nibli_auth import can, can_any, allowed_fields, explain

d = can("Alice", "update", "Doc1", "owns(Alice, Doc1).")
assert d.allowed
results = can_any("Alice", "read", ["Doc1", "Doc2"], "owns(Alice, Doc1).")
fields = allowed_fields("Alice", "read", "Doc1", ["title", "body"], "owns(Alice, Doc1).")

Package layout: extension nibli_auth_native (PyO3); helpers under python/nibli_auth/ (fastapi_ext, optional drf, strawberry_ext, graphene_ext, spectacular).

Identity header in demos: X-Agent (not a production auth scheme).

Examples (same policy)

ExampleCommandPort
auth-axumjust run-auth-axum3001
auth-fastapijust run-auth-fastapi3002

Scenarios: Alice owner, Bob stranger, Carol admin, Dave same-tenant read-only.

Extism

Extism is not the primary interface. A future PDK could wrap the same can / policy model; it is not implemented. Prefer the native Component Model / WIT path and in-process PyO3.

Versioning

LayerVersion
Policy file / POLICY_VERSION0.1.0
WIT packagenibli:engine@0.7.0 (authorizer export)
Crate nibli-authworkspace 0.1.0 (unpublished)

Do not invent predicates outside the fail-closed lexicon. Bump the policy version string when shipping incompatible rule changes.

Tests

just test-auth          # Rust core + axum/graphql/juniper features
just check-auth-axum
just test-auth-py       # requires maturin / .venv-auth (local)
just docs               # includes this chapter

Developer guide — overview

Audience: contributors to the compiler, reasoner, host, and CI gates.

The engine is one deterministic pipeline — nibli-kr → nibli-semantics → nibli-reason — shipped four ways (native library, WASM component + Wasmtime host, and two browser bundles), with every guarantee backed by a runnable gate. The chapters:

  • Crate map — the 22-crate workspace as a dependency graph: foundations, the compile chain, shared services, runtime surfaces, and tooling, with publish tiers.
  • Pipeline & IR — how KR text becomes an AstBuffer, then the flat LogicBuffer FOL IR, and what shapes the compiler guarantees (the emitted-shape contract).
  • WASM, host & compute — the single component, nibli-host mechanics (fuel, memory, trap recovery), the compute-backend protocol and its trust boundary, and the ship paths.
  • Soundness & CI index — every gate: the two-oracle differential track, the six Lean proofs and their conformance bridges, the umbrella recipes, fuzzing and mutation testing.
  • WIT surfacenibli:engine@0.7.0: every interface and session method, the bindings remap, and the version history.

Quick reference

TopicWhere
Logic IR (normative)LOGIC_IR.md
KR surface + pest grammarNIBLI_KR.md, nibli-kr/src/nibli_kr.pest
Soundness contractsGUARANTEES.md
Native CI gatejust ci (and just ci-wasm / just ci-all)
Deploy / playground shipDEPLOY.md
WIT boundarywit/world.wit (nibli:engine@0.7.0engine + authorizer)
AuthorizationUser guide: Authorization; crate nibli-auth; policy nibli-auth/policy/auth-0.1.0.nibli

Do not use the private book/ manuscript as a contributor reference for shipped behavior — prefer tests, gates, and the files above.

Crate map

The workspace has 22 member crates plus the deliberately excluded fuzz/ crate (root Cargo.toml: exclude = ["fuzz"]). Arrows below mean “depends on” (normal [dependencies] only — dev-dependencies are covered separately).

Two legibility omissions, disclosed: nibli-types edges are not drawn — most crates depend on it directly, and every crate except nibli-lexicon and lexigen at least transitively (nibli-store, nibli, nibli-auth-py, auth-axum, and fuzz reach it only through their deps) — and surfaces that wrap nibli-session also depend directly on the stage crates (nibli-kr / nibli-semantics / nibli-reason) for types and helpers — those direct edges are omitted where the session edge already implies the chain. Each crate’s Cargo.toml is the full truth.

flowchart TD
  subgraph foundations ["Foundations (dep-free)"]
    types["nibli-types<br/><i>canonical shared types</i>"]
    lexicon["nibli-lexicon<br/><i>committed English corpus</i>"]
  end

  subgraph chain ["Compile-and-reason chain"]
    kr["nibli-kr<br/><i>KR front-end (pest)</i>"]
    semantics["nibli-semantics<br/><i>AST → FOL LogicBuffer</i>"]
    reason["nibli-reason<br/><i>backward-chaining engine</i>"]
    session["nibli-session<br/><i>CoreSession — the ONE chain</i>"]
  end

  subgraph services ["Shared services"]
    protocol["nibli-protocol<br/><i>proof-trace wire format</i>"]
    render["nibli-render<br/><i>English rendering</i>"]
    store["nibli-store<br/><i>redb persistence</i>"]
  end

  subgraph runtimes ["Runtime surfaces"]
    engine["nibli-engine<br/><i>native embedding</i>"]
    pipeline["nibli-pipeline<br/><i>the WASM component</i>"]
    host["nibli-host<br/><i>Wasmtime host + REPL</i>"]
    wasm["nibli-wasm<br/><i>wasm-bindgen wrapper</i>"]
    ui["nibli-ui<br/><i>Dioxus playground</i>"]
  end

  subgraph tooling ["Tools, bins, extensions"]
    nibli["nibli<br/><i>6 dev bins incl. nibli-pin</i>"]
    import["nibli-import<br/><i>RDF/OWL import</i>"]
    formalize["nibli-formalize<br/><i>LLM English→KR gates</i>"]
    auth["nibli-auth<br/><i>logical authorization</i>"]
    authpy["nibli-auth-py<br/><i>PyO3 bindings</i>"]
    authaxum["auth-axum<br/><i>demo</i>"]
    verify["nibli-verify<br/><i>differential gates</i>"]
    lexigen["lexigen<br/><i>corpus refresh (report-only)</i>"]
    fuzz["nibli-fuzz<br/><i>3 libFuzzer targets</i>"]
  end

  kr --> lexicon
  semantics --> lexicon
  reason --> lexicon
  render --> protocol
  render --> lexicon
  session --> kr
  session --> semantics
  session --> reason
  store --> reason
  engine --> session
  engine --> store
  engine --> render
  engine --> protocol
  pipeline --> session
  pipeline --> auth
  host -. "loads nibli.wasm at runtime" .-> pipeline
  host --> kr
  host --> protocol
  host --> render
  host --> store
  wasm --> session
  wasm --> render
  wasm --> lexicon
  wasm --> protocol
  ui --> session
  ui --> formalize
  ui --> render
  ui --> lexicon
  ui --> protocol
  formalize --> kr
  formalize --> semantics
  formalize --> render
  formalize --> lexicon
  import --> engine
  nibli --> engine
  nibli --> import
  nibli --> render
  nibli --> kr
  auth --> session
  auth --> protocol
  authpy --> auth
  authaxum --> auth
  verify --> engine
  verify --> session
  verify --> lexicon
  lexigen --> lexicon
  fuzz --> engine
  fuzz --> kr
  fuzz --> semantics

The dashed edge is the one non-Cargo relationship in the graph: nibli-host does not link nibli-pipeline as a crate — it loads the built nibli.wasm component at runtime via Wasmtime (NIBLI_WASM_PATH).

Crate roster

Roles are taken from each crate’s own top-of-file doc comment. Tier is the crates.io publishing decision of record (RELEASING.md): Tier A is published to crates.io, in dependency order — see the API index for the live docs.rs links; Tier Z ships via GitHub Release / site / repo only.

CrateKindRoleTier
nibli-typeslibCanonical type definitions for the pipeline — one copy shared by every crate (LogicBuffer, AstBuffer, NibliError, shared arithmetic)A
nibli-lexiconlibThe committed English corpus: the dictionary is Rust source, const-validated, zero dependenciesA
nibli-protocollibProof-trace wire format (native serializes, browser deserializes); off-by-default compute-client feature holds the TCP client so wasm32 never pulls std::netA
nibli-krlibThe nibli KR surface-syntax front-end; the pest grammar file is the executable, normative grammarA
nibli-semanticslibFlat AST buffer → FOL LogicBuffer (an internal pipeline stage, not a standalone component)A
nibli-reasonlibThe inference engine: demand-driven backward chaining over an indexed fact store, proof tracesA
nibli-renderlibThe one place engine output becomes English; rendering is pure — never mutates a verdictA
nibli-sessionlibCoreSession: the single compile/assert/query chain every surface wraps — native↔WASM agreement by constructionA
nibli-storelib (+ a v2-fixture seed bin)Persistent redb store: ACID, postcard-serialized, tombstone retractionA (parenthesized)
nibli-enginelibNative in-process embedding — no WASM, full stack traces; the anchor of the publish orderA
nibli-formalizelibThe agentic English→KR formalizer engine (LLM behind real compile gates); UI shell lives in nibli-uiA (optional)
nibli-importlibRDF Turtle / OWL import + fact export (the nibli-import binary lives in the nibli crate)A (optional)
niblibin ×6Dev tooling: native REPL, nibli-validate, nibli-import CLI, nibli-pin, two bench binsA (optional)
nibli-pipelinecdylib+libThe WASM component: chains kr → semantics → reason as internal crate deps; the only crate with WIT bindingsZ
nibli-hostbinNative Wasmtime WASI P2 host + REPL; provides the compute-backend importZ
nibli-uibinThe Dioxus Transparency Triad playground; engine compiled into the bundle, reasons fully in-browserZ
nibli-wasmcdylib+rlibwasm-bindgen wrapper powering the live demo; mirrors nibli-engine’s no-store pathZ
nibli-verifylib+binThe differential soundness gates (Vampire / clingo oracles, seam gate, corpus differentials)Z
nibli-authlibBuilt-in authorization over a warm session core; publish = false today
nibli-auth-pycdylibPyO3 extension nibli_auth_native (maturin); publish = false
examples/auth-axumbinMinimal axum demo for nibli-auth; publish = false
tools/lexigenlib+binCorpus refresh tool (just regen-lexicon) — report-only, never rewrites entries, never a build-depZ
fuzz/ (nibli-fuzz)3 fuzz binslibFuzzer targets fuzz_assert / fuzz_query / fuzz_nibli_kr; excluded from the workspaceZ

Dev-dependency discipline

Several crates use the front-end as dev-dependencies only, keeping the release graph a strict DAG while letting tests build buffers the shipped way:

  • nibli-kr dev-depends on nibli-semantics (golden tests: every emitted buffer must be accepted by the semantic compiler).
  • nibli-reason dev-depends on nibli-kr + nibli-semantics (tests build event-decomposed LogicBuffers through the real front-end; the release build never links them).
  • nibli-render dev-depends on nibli-kr + nibli-semantics for the same reason.

Where the boundaries are

  • The one WASM boundary is host ↔ nibli-pipeline. Everything inside the component (nibli-krnibli-semanticsnibli-reason) is plain Rust function calls — see Pipeline & IR.
  • Foundations are dependency-free by design: nibli-lexicon has zero dependencies; nibli-types only an optional serde.
  • nibli-lexicon is the single arity sourcenibli-semantics delegates to it instead of keeping a parallel arity map, and lexigen pins against the compiled corpus rather than parsing source.

Pipeline & logic IR

Normative reference: LOGIC_IR.md at the repo root — the public spec of the LogicBuffer IR. This page is the guided tour; the spec wins on any disagreement. The single source of truth for the types is nibli-types/src/logic.rs.

The pipeline

flowchart LR
  txt["nibli KR text"] -->|nibli-kr| ast["AstBuffer"]
  ast -->|nibli-semantics| lb["LogicBuffer (FOL IR)"]
  lb -->|nibli-reason| v["TRUE / FALSE / UNKNOWN<br/>+ ProofTrace"]
  lb -.-> tptp["TPTP → Vampire<br/>(nibli-verify)"]
  lb -.-> asp["ASP → clingo<br/>(nibli-verify)"]
  lb -.-> eng["English<br/>(nibli-render)"]

The three stages are plain Rust function callsAstBuffer never crosses a WASM boundary, and nibli-semantics is an internal stage, not a component. The one WASM boundary is host ↔ nibli-pipeline, and only LogicBuffer (flat, u32-indexed, no pointers) crosses it. The LogicBuffer is the language-agnostic seam: only nibli-kr (parser) and nibli-lexicon (dictionary) are front-end-specific; the reasoner, both differential oracles, and the Lean proofs operate on or below the IR.

Queries and assertions use the same compiler — there is no separate query syntax at the IR level; divergence is entirely post-buffer.

CoreSession — the one compile chain

nibli_session::CoreSession packages the chain (nibli_kr::parse_checkednibli_semantics::compile_from_astnibli_reason::transform_compute_nodes) plus the compute-predicate registry and the assert/query verbs. Every runtime surface — nibli-engine (native), nibli-pipeline (WASM component), nibli-wasm and nibli-ui (browser) — wraps it with only boundary conversion, so native↔WASM agreement holds by construction. Per-surface policy (error conversion, lint notes, env reads, persistence, compute-dispatch wiring) deliberately stays outside the core.

AstBuffer (internal interchange)

The parser’s output and the semantic compiler’s input: parallel u32-indexed arrays (predicates / arguments / sentences / roots). It lives in nibli-types (not nibli-kr) because it is also nibli_kr::render’s input (the round-trip layer) and the validated programmatic-build target — hand-built buffers pass validate_ast_buffer (index bounds, acyclicity, the $-sigil invariant on variables) before compilation. Arguments are typed: Variable (sigiled $name), Marker (it / slot / ?), Pronoun (a closed 14-variant inventory), plus names, descriptions, numbers — there is no string-sniffed catch-all.

LogicBuffer (the FOL IR)

Two fields, no version field:

#![allow(unused)]
fn main() {
pub struct LogicBuffer {
    pub nodes: Vec<LogicNode>,
    pub roots: Vec<u32>,   // top-level formula nodes
}
}

13 LogicNode variants: Predicate, ComputeNode (an atom dispatched to the compute backend), AndNode, OrNode, NotNode, ExistsNode, ForAllNode, PastNode / PresentNode / FutureNode (tense), ObligatoryNode / PermittedNode (deontic), CountNode (“exactly N”). 5 LogicalTerm variants: Variable, Constant, Description, Unspecified, Number(f64). Spec and code match one-for-one in name, payload, and declaration order. Adding a variant is a breaking change across every conversion site; an in-source guard (__exhaustiveness_guard in logic.rs) forces that breakage to land in one documented location whose checklist names each site to update — including the ones no compiler error reaches (the WIT variant case + bindings regenerate, and the serde round-trip test).

Structural guarantees: post-order layout (children precede parents), DAG-not-tree (the flattener shares subtrees; acyclicity is the producer’s responsibility — the reasoner bounds-checks, it does not cycle-validate), and root granularity = fact granularity (split_roots() shares the whole node arena, exposing one root per fact). One footgun is flagged in both spec and code: CountNode’s middle field is a count, not a node index — the only non-index u32 payload in the IR.

There are deliberately no Biconditional/Xor nodes: the flattener expands A <-> B and A ⊕ B into And/Or/Not shapes (sharing subtrees) before the buffer exists.

Emitted-shape invariants (the contract)

These shapes are contract, not accident — pinned by the seam-conformance gate (just verify-nibli-kr-seam):

  • Neo-Davidsonian event decomposition. dog(Adam). compiles to ∃ev. dog(ev) ∧ dog_x1(ev, adam) ∧ dog_x2(ev, Unspecified) — a unary type predicate over a fresh event variable plus one binary role predicate per dictionary place (dog is arity-2: x2 is the breed), unfilled places padded with Unspecified so role predicates stay arity-consistent.
  • Quantifier shapes. some dogExists(v, And(restrictor, body)); every dogForAll(v, Or(Not(restrictor), body)) (the implication arrow); exactly NCountNode. Prenex all $x: … wraps the body directly — no restrictor, no arrow.
  • Flat-atom families. Not everything is event-decomposed: equals (the identity) stays a flat 2-argument atom (the union-find ingests exactly that shape); via modal tags, the the_domain_<name> restrictors, and the abstraction type predicates are also flat.
  • Abstraction opacity. event { P() } bodies compile behind a content-hashed __abs_<hash> marker: the reasoner matches the marker but skips the body, so asserting believe(me, fact { P }) never makes bare P true.
  • Compute transform. The front-end never emits ComputeNode; nibli_reason::transform_compute_nodes rewrites marked Predicates after compilation. BYO-buffer users must run it themselves — a compute relation left as a plain Predicate is treated as an ordinary fact.

NotNode is structurally plain ¬ — the closed-world reading is a reasoner property, carried on the verdict side by ProofTrace.naf_dependent and ProofTrace.cwa_false.

Entry points

You wantUse
Text → IRNibliEngine::compile_debug (native), compile-debug (WIT), or nibli_semantics::compile_from_ast (+ transform_compute_nodes)
A programmatic ground factnibli_semantics::compile_injected_fact(relation, args) — decomposes and pads exactly like surface text
Reason over a buffer (BYO-IR)nibli_reason::KnowledgeBase: assert_fact, query_entailment[_with_proof], query_find, count_witnesses, aggregate, with_assumptions, retract_fact
A packaged surfacenibli_engine::NibliEngine (native), the nibli-wasm Session (browser JS), or the nibli-pipeline component (WIT surface)

Stable vs internal

Stable: the 13+5 variant inventories (names, payloads, declaration order); the two-field buffer; post-order layout; root granularity + split_roots; the emitted-shape invariants; the ProofRule/ProofStep/ProofTrace JSON contract; the NibliError display prefixes; the WIT logic-types interface.

Internal: variable/Skolem naming (_v0, sk_N), __abs_ hash digits, concrete index values, the compiler’s tree IR (IrForm), stored-fact forms, on-disk mirrors.

The buffer has no version field — the WIT package version (nibli:engine@0.7.0) and nibli-store’s fail-closed schema versions are the only version markers. Treat the format as pre-1.0: pin a commit if you build against it.

Writing a consumer or producer

The two shipped external consumers are the templates: nibli-verify/src/tptp.rs (→ Vampire; hard-errors on out-of-fragment nodes rather than mistranslating) and nibli-verify/src/asp.rs (→ clingo; regroups the event decomposition back to surface atoms). A producer must emit the invariant shapes — most importantly the event decomposition with consistent role arities, the ∀-implication arrow, and flat 2-arg equals — and gets soundness checking for free: the reasoner rejects non-stratifiable rule sets at assert time.

WASM, host & compute

How the engine ships: one WASM component, a native Wasmtime host, an optional external compute backend, and two browser surfaces that skip WASI entirely.

Sources: nibli-host/src/main.rs, nibli-protocol/src/compute_client.rs, python/nibli_backend.py, DEPLOY.md.

One component, four runtimes

nibli-pipeline is the single WASM component (WIT world nibli-pipeline): nibli-kr / nibli-semantics / nibli-reason are internal Rust crate dependencies, not separate components. It imports compute-backend from the host and exports the engine + authorizer interfaces (WIT surface).

RuntimeWhat runsCompute dispatch
nibli-host (Wasmtime, WASI P2)loads nibli.wasm; the canonical operator REPLThe component registers dispatch at Session creation, bridging to the host’s compute-backend implementation
nibli-engine (native, in-process)the same crates as plain RustOpt-in: enable_compute_backend(addr) wires the native TCP client; otherwise external compute stays unregistered
nibli-wasm / nibli-ui (browser, wasm32-unknown-unknown)the same crates via wasm-bindgen / DioxusExternal compute deliberately unregistered (no TCP in the browser); built-in arithmetic still resolves in-engine

All four wrap the same nibli_session::CoreSession, so they agree by construction. The dispatch hook is per-KB-instance function pointers (KnowledgeBase::set_compute_dispatch) — nibli-reason itself holds no thread-locals or globals, which is what lets multithreaded embedders register it at all. The native TCP client those pointers call into does live per-thread in the embedder (nibli-engine/src/compute_client.rs), so a multithreaded native embedder calls enable_compute_backend on each worker thread that reasons.

nibli-host mechanics

just run builds the component and launches the host (NIBLI_WASM_PATH=target/wasm32-wasip2/debug/nibli.wasm by default).

Environment variables (read at startup):

VarDefaultEffect
NIBLI_WASM_PATHtarget/wasm32-wasip2/debug/nibli.wasmComponent location
NIBLI_FUEL50_000_000_000Wasmtime fuel budget per command (debug WASM is ~6× hungrier than release; ~1.5e11 covers the heaviest demo corpus on debug)
NIBLI_MEMORY_MB512Guest memory cap (trap_on_grow_failure)
NIBLI_COMPUTE_ADDRunsetExternal backend host:port; unset = built-in arithmetic only
NIBLI_DB_PATHunsetOptional persistent redb store (migrated + replayed fail-closed at startup)
NIBLI_QUIEToff=1 suppresses the [Fact #N] / [Skolem] / [Rule] bookkeeping echoes (forwarded into the guest’s WASI env)
NIBLI_STRICToff=1 makes arity/integrity violations reject atomically instead of warn-and-insert
NIBLI_EXISTENTIAL_IMPORTon=0 gives the clean-core profile: no presupposition witness, some = plain classical ∃
NIBLI_MATERIALIZEon=0 opts out of NAF saturation, sending every NAF check back through backward chaining

Runtime toggles: :fuel [n], :memory [mb], :backend [addr], :strict on|off, :existential-import on|off, :materialize on|off (bare :materialize prints the saturation report). Script mode (--script <file> or piped stdin) captures transcripts byte-faithfully.

Resource traps don’t brick the session. Fuel exhaustion (wasmtime::Trap::OutOfFuel) and memory-grow denials are classified and — for queries — synthesized into a RESOURCE_EXCEEDED (fuel|memory) verdict with a remediation hint. A trap poisons the component instance, so the host keeps a journal of every successful KB mutation and lazily rebuilds a byte-identical session on the next call (the engine is deterministic: identical fact ids and Skolem numbering). Raising :fuel between trap and re-query applies to the replay. Depth limits are engine-level, never a trap.

The compute backend

An external process the reasoner can consult for computed predicates — JSON Lines over TCP, one object per line:

{"relation": "exponential", "args": [{"type": "number", "value": 8.0}, {"type": "number", "value": 2.0}, {"type": "number", "value": 3.0}]}
{"result": true}

Responses are {"result": true|false} or {"error": "..."}. Argument tags: variable, constant, description, unspecified, number.

  • Built-in vs forwarded: product / sum / quotient with fully numeric arguments evaluate locally (the shared nibli_types::eval_arithmetic); a call whose arguments don’t resolve to numbers falls through to the backend — which is why the reference server implements all three too. Everything else registered via :compute <name> forwards.
  • Tolerant equality (disclosed): arithmetic equality is isclose with rel_tol 1e-9, abs_tol 00.3 = 0.1 + 0.2 is TRUE. The comparison predicate num_equal is exact ==. Non-finite operands yield UNKNOWN (non-finite).
  • Trust boundary (disclosed): a backend true reply is auto-asserted as a ground fact mid-query which downstream rules chain on. The backend is part of the trusted computing base — a plaintext, unauthenticated peer; run it on localhost or a segment you control. Auto-asserted compute facts are non-durable — never journaled or replayed, recomputed on demand, and they do not survive a restart (the persistent engine’s typed mirror is cleared and rebuilt from the fact registry on open).
  • No backend configured? A registered external predicate answers UNKNOWN (backend-unavailable) — an outage is never a derived falsehood (pinned by the smoke-host-backend-unavailable gate).
  • Client behavior: lazy connect, reused connection (idle reap after 300 s, read timeout 10 s, write 5 s — NIBLI_BACKEND_* env overrides), retry-once on connection errors, and a batch path that pipelines all requests in one burst to amortize WASM-boundary and TCP round trips.

The reference server is python/nibli_backend.py (just backend, port 5555): handlers product, sum, quotient, exponential, logarithm in a HANDLERS dict — extend by adding an entry. just run-with-backend wires host + backend together.

Browser surfaces

No WASI, no component, no server: nibli-wasm (wasm-bindgen Session for JS; powers the live demo) and nibli-ui (the Dioxus playground) compile the engine crates straight into the browser bundle. nibli-wasm keeps two deprecated no-op shims (set_language, back_translate) for deployed-site compatibility until the site migration lands; the live back-translation is IR-driven (back_translate_ir).

Ship paths

TargetWhatHow
dhilipsiva.dev/nibli-playgroundnibli-ui bundleBuilt by the external dhilipsiva.dev site repo; this repo pings it via redeploy-site.yml (repository_dispatch: nibli-updated) on every push to main — self-skips until the SITE_DISPATCH_TOKEN secret exists
dhilipsiva.dev/niblinibli-wasm live demoSame site repo
dhilipsiva.github.io/nibli/this docs site (mirror)docs-pages.yml: just docs /nibli/ → GitHub Pages, on any mdbook/** / Justfile / flake push
dhilipsiva.dev/docs/nibli/this docs site (primary, pending)Site repo copies the default just docs build (DEPLOY.md §2b)

just build-ui produces the exact shipping bundle locally (target/dx/nibli-ui/release/web/public/) as a pre-merge sanity check — the production build runs in the site repo. Since the committed corpus, no build needs a dictionary fetch — the full vocabulary is compiled in (the site repo’s leftover fetch step is obsolete and unread; see DEPLOY.md).

Soundness & CI index

Every guarantee in GUARANTEES.md is backed by a gate you can run. This page indexes them: what each gate checks, what it needs, and when it runs. The Justfile is the source of truth for recipe contents.

The umbrella recipes

RecipeWhat it is
just ciThe fast native gate (no WASM build): fmt-check, release-check, clippy-runtime, the unit sweep (test), test-engine, test-host, test-ui, test-formalize, test-backend, test-store, test-persistence-replay, and every verify-* gate in the table below — including verify-proofs (plus verify-book-vocab, which self-skips when the private book/ checkout is absent)
just ci-wasmThe WASM behavioral gate: builds the component + host once, then runs 15 smoke-host-* scripts (trap recovery, persist-replay, statement split, schema-v3 migration, NAF note, CWA-false note, :debug, proof collapse, backend-unavailable, quiet, strict, existential-import, materialize, determinism, script) plus verify-wasm-node
just ci-allci + ci-wasm — the comprehensive pre-push gate

GitHub Actions (.github/workflows/ci.yml) runs four parallel jobs, all inside the Nix shell: runtime-ci (just build-wasm — bindings are generated, gitignored, absent on fresh checkouts — then just ci-all), auth (just test-auth + check-auth-axum), docs (just docs), and fuzz (just fuzz-ci, time-boxed).

The verify-* gates

Track A proper is the differential-oracle family (the first row); the rest are supporting native gates that ride the same ci umbrella.

GateOracle / methodNotes
just verify-soundness (Track A)Vampire (classical FOL over the Horn/NAF-free fragment, via TPTP) and clingo (ASP over the stratified-NAF + closed-world fragment) check nibli’s verdicts on curated + seeded-random programs, the mappable corpus slices, and the Predilex taxonomy leg; plus the non-stratified-rejection differential (every accept/reject decision checked against an independent implementation of the proven criterion, with a post-rejection fresh-replay battery) and two engine-vs-engine metamorphic differentials: retraction (retract ≡ never-asserted) and materialisation (saturated ≡ backward-chained)Solvers come from the Nix shell; each oracle side skips cleanly if its solver is absent. Batch sizes via NIBLI_VERIFY_RANDOM_COUNT (200), …_NAF_… (100), …_TENSE_NAF_… (100), …_COUNT_… (100), …_STRAT_… (300), …_RETRACT_… (200), …_MATERIALIZE_… (60)
just verify-nibli-kr-seamHand-verified FOL structural goldens + the full construct-inventory acceptance sweep + KR-internal metamorphic relations + the native determinism legThe front-end’s oracle; never skips
just verify-alias-mapThe committed corpus’s shape/provenance/compound invariants + a behavioral twin for every shipped entry (named ≡ positional; converted ≡ canonical base)Never skips
just verify-dictCorpus arities must cover independent Predilex lower bounds (vendored, SHA-pinned), keyed through the gismu provenance bridge132 words checked, floor 120; arity-only scope
just verify-pinsKB-level behavioural pins (pins/*.nibli) run by the native nibli-pin runnerDistinct exit codes: 1 = pinned property regressed, 2 = harness broken, 3 = a pinned defect no longer reproduces
just verify-harnessThe known-failures control tests (the FOL control must pass; the red backlog stays opt-in)
just verify-doc-fencesEvery statement in a ```nibli-kr fence — under mdbook/src/ and in the repo-root specs — must compile through NibliEngine::assert_text, the same path as the REPL’s :loadLives here, not in the docs CI job, which has no Rust toolchain and stays a ~2-minute mdbook build. The scope boundary is compilable example vs. metasyntax, carried by the info string: notation and the v2-only pred declarations sit in ```text fences, because tagging either ```nibli-kr claims it compiles today
just verify-grammar-paritygrammars/nibli.tmLanguage.json’s keyword alternation must equal nibli_lexicon::RESERVED_WORDS — set, order, and the \b anchorsThe shipped TextMate grammar is a third mirror of the keyword list; the pest twin was already pinned inside nibli-kr, this closes the last one

Determinism is a three-way gate: the same pinned corpus (determinism-corpus.nibli) must produce identical verdicts on the native engine (in verify-nibli-kr-seam), the Wasmtime component (smoke-host-determinism), and node/V8 (verify-wasm-node, skips without wasm-pack) — the browser-class runtime of the live playground.

Track B — mechanized proofs

Six Lean 4 proofs in proofs/ (no mathlib — self-contained, offline), checked by just verify-proofs (skips cleanly without lean; the Nix shell provides it). Each proof is bridged to the real engine by a Rust conformance test that runs even when Lean skips:

ProofWhat it provesRust bridge
Combiner.leanThe four-valued verdict combiner never fabricates a definitive verdict nor swallows a non-definitive siblingexhaustive_soundness_matches_lean_model — all 10×10 inputs, so the guarantee is complete
Stratification.leanThe NAF criterion (“no negative edge whose target reaches back to its source”) is equivalent to a valid stratification existingcheck_stratification_matches_proven_criterion
Scc.leanThe SCC-based check the engine actually runs equals the proven reachability criterioncompute_sccs_matches_scc_spec
Unify.leanThe one-directional unifier is sound and minimal (subst σ t = c; never binds a variable absent from the template)unify_conformance
RuleFiring.leanUniversal-rule firing is sound modus ponens — a model-sound rule can never conclude a goal outside the modelrule_firing_conformance
Trace.lean (capstone)A recorded proof trace, read as a certificate, is sound w.r.t. the stratified perfect model: TRUE ⇒ in the model, closed-world FALSE ⇒ not in ittrace_soundness_conformance (bridges each model axiom, with exercised-counters)

Honest scope (stated in proofs/README.md and GUARANTEES): the proofs are model-level plus corpus conformance tests — not one end-to-end machine-checked pipeline from source text to model. The KR→semantics seam is conformance-gated (the seam gate), which narrows but does not close that gap.

On-demand gates

RecipeWhat
just fuzz-ci [SECS]Seeds fuzz/corpus/ from the shipped .nibli files, then runs all three libFuzzer targets (fuzz_assert, fuzz_query, fuzz_nibli_kr) time-boxed; crash/OOM/leak fails. Runs as the parallel fuzz CI job. Needs the Nix shell’s pinned nightly (NIBLI_NIGHTLY_BIN)
just mutants [JOBS]Mutation testing over the soundness paths (scope in .cargo/mutants.toml); each process capped at 12 GiB so runaway mutants die alone as a catch; survivors diffed against mutants-baseline.txt — any new survivor fails. ~2.5 h full sweep; use cargo mutants --in-diff for incremental changes
just bench-naf / just bench-bookRelease-profile timing pins — the only legitimate source for any quoted latency figure
just count-testsDerives test-suite counts — the only legitimate source for any quoted test count

Reading GUARANTEES.md

The contract document is organized as: Front-End Language, Soundness (Tracks A + B, the seam gate, the mutation baseline, determinism), Completeness, Negation Policy, Equality Semantics, Predicate Validation, Integrity Constraints, Resource Limits, Retraction Model, Query Result Contract, Hypothetical Reasoning, Aggregation, Disclosed Sharp Edges, What the Engine Cannot Do, Closed Base Vocabulary, and Time and Order. Its core statement: if the engine says TRUE, a formal derivation from your asserted facts and rules exists — and the proof trace shows it.

WIT surface

The component boundary, as declared in wit/world.wit — package nibli:engine@0.7.0, world nibli-pipeline:

world nibli-pipeline {
    import compute-backend;
    export engine;
    export authorizer;
}

One component, one world. The WIT package version is independent of crate semver (a locked decision of record). Only flat, u32-indexed data crosses the boundary — no heap pointers.

Interfaces

error-types

variant nibli-error identifies the pipeline stage that failed: syntax(syntax-detail) (with message + 1-based line/column), semantic(string), reasoning(string), backend(tuple<string, string>).

logic-types

The IR and verdict types (Pipeline & IR):

  • logical-term (5 cases) and logic-node (13 cases) mirror nibli_types::logic exactly — kebab-case names, identical declaration order (the component-model discriminant is positional).
  • logic-buffer { nodes, roots }.
  • query-result: true | false | unknown(unknown-reason) | resource-exceeded(resource-kind), with unknown-reason ∈ {cycle-cut, incomplete-knowledge, naf-dependent, backend-unavailable, non-finite} and resource-kind ∈ {depth, fuel, memory}.
  • witness-binding { variable, term }, fact-id (u64), fact-summary { id, label, root-count }.
  • proof-rule (19 cases) + 15 named-field payload records (WIT variant cases hold at most one payload type, so each data-carrying case gets a record — the interface self-documents instead of using positional tuples); proof-step { rule, holds, children }; proof-trace { steps, root, naf-dependent, cwa-false } — the NAF/CWA flags are computed once in the engine and carried across so consumers never recompute them.
  • materialization-report { complete, refused } — which relations the NAF saturation completed, and which it refused with a one-line reason each.

compute-backend (the host import)

  • evaluate(relation, args) -> result<bool, nibli-error>
  • evaluate-batch(requests) -> list<compute-result> — one boundary crossing for N requests; results in input order, one failure never poisons the batch.

The engine calls this for predicates registered for compute dispatch; the host answers built-in arithmetic locally and forwards the rest over TCP (WASM, host & compute).

engine (export) — the session resource

MethodContract
constructor()Fresh KB
assert-text(input)list<(fact-id, logic-buffer)> — multi-statement input splits into one independent fact per root (a connective stays one compound fact); each pair carries the compiled buffer so a persisting host can replay without recompiling
query-text(input)query-result
query-text-with-proof(input)(query-result, proof-trace)
query-find-text(input)→ witness binding sets
compile-debug(input)Compile without asserting; the host renders the buffer
assert-fact(relation, args) / assert-fact-with-id(…)Ground fact, bypassing text parsing; the -with-id form takes a caller-chosen id for restart replay
assert-buffer-with-id(buffer, label, id)The recompile-free replay primitive (the legacy assert-text-with-id was removed at 0.5.0 with store schema v3)
retract-fact(id) / list-facts() / reset-kb()KB management
register-compute-predicate(name)Marks a relation for compute dispatch
set-strict(bool)Off = permissive warn-and-insert; on = arity/integrity violations reject atomically
set-existential-import(bool)Default on (a description universal mints a presupposition witness); off = clean-core classical ∃
set-materialization(bool) / materialization-report()NAF saturation toggle + its report — added in 0.7.0 because the optimisation is invisible when it fails: without the report, a KB whose ~p(x) stays slow has no way to learn which relation fell out of the materialisable fragment, or why. A definitive TRUE/FALSE can never flip (the materialize_diff gate enforces it); a non-definitive OFF verdict may become definitive under ON — the deliberate depth-bound completeness gain

authorizer (export)

The built-in authorization surface (logical auth v0.1), wrapping the same policy as the native nibli-auth crate. Types: decision { allowed, verdict, reason, fields } (allowed is true only on entailment TRUE) and explained { decision, proof-json }. Session methods: load-policy, assert-facts, retract, can, can-any (batch), allowed-fields, explain, policy-version, clear-ephemeral. Two conventions worth knowing: the protected-resource parameter is named object (WIT reserves the keyword resource), and error results are plain string, not nibli-error.

Bindings: the types ARE nibli_types

nibli-pipeline is the only crate with WIT bindings, regenerated by cargo component build -p nibli-pipeline (the just build-wasm recipe, which also cargo fmts the output; src/bindings.rs is gitignored — CI regenerates it on every run).

[package.metadata.component.bindings.with] remaps the ten ABI-matching boundary types onto the canonical nibli_types definitions — logical-term, logic-node, logic-buffer, query-result, unknown-reason, resource-kind, witness-binding, fact-summary from logic-types, plus nibli-error and syntax-detail from error-types — so the guest passes the canonical types straight through instead of maintaining a mirror-conversion layer.

The one exception is the proof trio: proof-rule is named-field in Rust but wit-bindgen emits only tuple/newtype variants, so it keeps the single hand-written convert_proof_rule bridge in nibli-pipeline/src/lib.rs (proof-step/proof-trace reference it and stay generated too).

Version-bump checklist: the remap keys pin the interface version (nibli:engine/logic-types@0.7.0/…). Any WIT version change must bump all ten keys — a missed key silently stops remapping and resurrects the mirror types.

Version history

WITChange
0.7.0set-materialization + materialization-report
0.6.0export authorizer (wrapping native nibli-auth)
0.5.0Removed legacy assert-text-with-id (store schema v3)
0.4.0set-existential-import
0.3.0Named-field proof-rule payload records
0.2.0set-language (no longer present in the current WIT — the Lojban front-end retired at THE DROP)

Engine specifications

Normative and operational specs live at the repository root (and in-tree sources). This page is a link-out index — not a second copy of those files.

DocumentPathRole
Product / quickstartREADME.mdOverview, REPL, playground, compute backend
nibli KR languageNIBLI_KR.mdNormative surface syntax (v0.1)
Executable grammarnibli-kr/src/nibli_kr.pestParser source of truth
Logic IRLOGIC_IR.mdLogicBuffer / FOL intermediate form
GuaranteesGUARANTEES.mdSoundness, gates, contracts
DeployDEPLOY.mdPlayground / static ship path
WITwit/world.witComponent boundary
ReleasingRELEASING.mdTier A/Z decision table + operator runbook
RoadmapTODO.mdOpen engine, tooling and docs work
Lexiconnibli-lexicon/src/corpus/Committed English predicate corpus
Authorizationnibli-auth, policy auth-0.1.0.nibliBuiltin auth; guide: Authorization
WIT packagenibli:engine@0.7.0Exports engine + authorizer (object = resource id)

API documentation

Rust crate APIs are on docs.rs — every published crate is listed with its versioned link in the API index, starting from docs.rs/nibli-engine. From a checkout, cargo doc -p <crate> --open builds the same rustdoc locally. That is separate from this mdBook site, which carries the conceptual docs only.

Hosting note

HostBase pathStatus
GH Pages mirrorhttps://dhilipsiva.github.io/nibli/CI workflow docs-pages.yml (site-url=/nibli/)
Primaryhttps://dhilipsiva.dev/docs/nibli/Site-repo integration (see DEPLOY.md) — default just docs build
Localjust docs-servehttp://127.0.0.1:3000Available now

API index

Rustdoc for the embeddable crates, on docs.rs since the first publish (v0.1.0, 2026-08-03). Embedding starts with one line:

cargo add nibli-engine

Every crate below is version 0.1.0 in workspace lockstep, MIT OR Apache-2.0, published in the dependency order the Tier A/Z decision table in RELEASING.md locks. (docs.rs builds each crate shortly after publish — a page may briefly show “building” right after a release.) For local API docs from a checkout, cargo doc -p <crate> --open still works.

Publishable crates (Tier A, in dependency order)

CrateAPI docsWhat its API gives you
nibli-typesdocs.rs/nibli-types/0.1.0Canonical type definitions shared across the pipeline — LogicBuffer, AstBuffer, NibliError, the shared arithmetic evaluator
nibli-lexicondocs.rs/nibli-lexicon/0.1.0The committed English predicate corpus — lookups, place labels, provenance bridge
nibli-protocoldocs.rs/nibli-protocol/0.1.0Shared wire-format proof-trace types and the JSON helpers
nibli-krdocs.rs/nibli-kr/0.1.0The nibli KR surface-syntax front-end — parse_checked, parse_text, render, the pest grammar
nibli-semanticsdocs.rs/nibli-semantics/0.1.0Semantic compiler — flat AST buffer to First-Order Logic IR
nibli-reasondocs.rs/nibli-reason/0.1.0Reasoning engine — backward-chaining inference over the typed fact store, proof traces
nibli-renderdocs.rs/nibli-render/0.1.0Shared human-readable rendering for back-translation and proof traces
nibli-sessiondocs.rs/nibli-session/0.1.0The shared session core: the one compile/assert/query chain every runtime surface wraps
(nibli-store)docs.rs/nibli-store/0.1.0Persistent redb-backed knowledge-base store with tombstone retraction — parenthesized in the decision table: needed only when embedding with persistence
nibli-enginedocs.rs/nibli-engine/0.1.0The native embedding — NibliEngine: assert_text, query_text_with_proof, query_find_text, retract_fact, optional persistence. Start here for embedding
nibli-formalize (optional)docs.rs/nibli-formalize/0.1.0Agentic English→KR formalizer: LLM + validation gates + self-correction loop
nibli-import (optional)docs.rs/nibli-import/0.1.0RDF/OWL import and KB export utilities
nibli (optional)docs.rs/nibli/0.1.0The dev bins: native REPL, nibli-validate, nibli-import CLI, nibli-pin (bin-only; the bench bins are repo-only behind the bench-bins feature)

Not on the list

The Tier Z crates (nibli-pipeline, nibli-host, nibli-ui, nibli-wasm, nibli-verify, nibli-lexigen, plus the workspace-excluded fuzz/ harness) are publish = false: they ship as the WASM component, host binary, hosted sites, and CI gates rather than as libraries — the v0.1.0 GitHub Release carries the built component and host. Their internals are covered by the developer guide. The auth crates (nibli-auth, nibli-auth-py) and the auth-axum example are also publish = false for now — they sit in neither tier row of the decision table; the authorization guide covers their APIs.

Conceptual documentation (this site) stays on the primary/mirror hosts only — docs.rs carries API docs, not these pages.